mirror of
https://github.com/openai/codex.git
synced 2026-11-10 21:00:47 +00:00
Add plugin hook enablement config APIs
This commit is contained in:
Generated
+1
@@ -1856,6 +1856,7 @@ dependencies = [
|
||||
"codex-feedback",
|
||||
"codex-file-search",
|
||||
"codex-git-utils",
|
||||
"codex-hooks",
|
||||
"codex-login",
|
||||
"codex-mcp",
|
||||
"codex-model-provider",
|
||||
|
||||
@@ -1447,6 +1447,52 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookConfigSource": {
|
||||
"enum": [
|
||||
"plugin"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HooksConfigWriteParams": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/HookConfigSource"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"key",
|
||||
"source"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HooksListParams": {
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"description": "When omitted or empty, defaults to the current session working directory.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"ImageDetail": {
|
||||
"enum": [
|
||||
"auto",
|
||||
@@ -5336,6 +5382,54 @@
|
||||
"title": "Skills/config/writeRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"hooks/list"
|
||||
],
|
||||
"title": "Hooks/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/HooksListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Hooks/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"hooks/config/write"
|
||||
],
|
||||
"title": "Hooks/config/writeRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/HooksConfigWriteParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Hooks/config/writeRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
+236
@@ -1098,6 +1098,54 @@
|
||||
"title": "Skills/config/writeRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"hooks/list"
|
||||
],
|
||||
"title": "Hooks/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/HooksListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Hooks/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"hooks/config/write"
|
||||
],
|
||||
"title": "Hooks/config/writeRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/HooksConfigWriteParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Hooks/config/writeRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -9567,6 +9615,27 @@
|
||||
"title": "HookCompletedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"HookConfigSource": {
|
||||
"enum": [
|
||||
"plugin"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookErrorInfo": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookEventName": {
|
||||
"enum": [
|
||||
"preToolUse",
|
||||
@@ -9593,6 +9662,75 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookMetadata": {
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"eventName": {
|
||||
"$ref": "#/definitions/v2/HookEventName"
|
||||
},
|
||||
"handlerType": {
|
||||
"$ref": "#/definitions/v2/HookHandlerType"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"matcher": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/v2/HookSource"
|
||||
},
|
||||
"sourcePath": {
|
||||
"$ref": "#/definitions/v2/AbsolutePathBuf"
|
||||
},
|
||||
"sourceRelativePath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"timeoutSec": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"eventName",
|
||||
"handlerType",
|
||||
"key",
|
||||
"source",
|
||||
"sourcePath"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookOutputEntry": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
@@ -9767,6 +9905,104 @@
|
||||
"title": "HookStartedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"HooksConfigWriteParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/v2/HookConfigSource"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"key",
|
||||
"source"
|
||||
],
|
||||
"title": "HooksConfigWriteParams",
|
||||
"type": "object"
|
||||
},
|
||||
"HooksConfigWriteResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"effectiveEnabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"effectiveEnabled"
|
||||
],
|
||||
"title": "HooksConfigWriteResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"HooksListEntry": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"errors": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/HookErrorInfo"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"hooks": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/HookMetadata"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"errors",
|
||||
"hooks"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HooksListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"description": "When omitted or empty, defaults to the current session working directory.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "HooksListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"HooksListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/HooksListEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "HooksListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ImageDetail": {
|
||||
"enum": [
|
||||
"auto",
|
||||
|
||||
+236
@@ -1804,6 +1804,54 @@
|
||||
"title": "Skills/config/writeRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"hooks/list"
|
||||
],
|
||||
"title": "Hooks/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/HooksListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Hooks/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"hooks/config/write"
|
||||
],
|
||||
"title": "Hooks/config/writeRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/HooksConfigWriteParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Hooks/config/writeRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -6197,6 +6245,27 @@
|
||||
"title": "HookCompletedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"HookConfigSource": {
|
||||
"enum": [
|
||||
"plugin"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookErrorInfo": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookEventName": {
|
||||
"enum": [
|
||||
"preToolUse",
|
||||
@@ -6223,6 +6292,75 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookMetadata": {
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"eventName": {
|
||||
"$ref": "#/definitions/HookEventName"
|
||||
},
|
||||
"handlerType": {
|
||||
"$ref": "#/definitions/HookHandlerType"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"matcher": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/HookSource"
|
||||
},
|
||||
"sourcePath": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"sourceRelativePath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"timeoutSec": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"eventName",
|
||||
"handlerType",
|
||||
"key",
|
||||
"source",
|
||||
"sourcePath"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookOutputEntry": {
|
||||
"properties": {
|
||||
"kind": {
|
||||
@@ -6397,6 +6535,104 @@
|
||||
"title": "HookStartedNotification",
|
||||
"type": "object"
|
||||
},
|
||||
"HooksConfigWriteParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/HookConfigSource"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"key",
|
||||
"source"
|
||||
],
|
||||
"title": "HooksConfigWriteParams",
|
||||
"type": "object"
|
||||
},
|
||||
"HooksConfigWriteResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"effectiveEnabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"effectiveEnabled"
|
||||
],
|
||||
"title": "HooksConfigWriteResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"HooksListEntry": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"errors": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookErrorInfo"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"hooks": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookMetadata"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"errors",
|
||||
"hooks"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HooksListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"description": "When omitted or empty, defaults to the current session working directory.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "HooksListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"HooksListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HooksListEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "HooksListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"ImageDetail": {
|
||||
"enum": [
|
||||
"auto",
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"HookConfigSource": {
|
||||
"enum": [
|
||||
"plugin"
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/HookConfigSource"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"key",
|
||||
"source"
|
||||
],
|
||||
"title": "HooksConfigWriteParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"effectiveEnabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"effectiveEnabled"
|
||||
],
|
||||
"title": "HooksConfigWriteResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"cwds": {
|
||||
"description": "When omitted or empty, defaults to the current session working directory.",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"title": "HooksListParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"definitions": {
|
||||
"AbsolutePathBuf": {
|
||||
"description": "A path that is guaranteed to be absolute and normalized (though it is not guaranteed to be canonicalized or exist on the filesystem).\n\nIMPORTANT: When deserializing an `AbsolutePathBuf`, a base path must be set using [AbsolutePathBufGuard::new]. If no base path is set, the deserialization will fail unless the path being deserialized is already absolute.",
|
||||
"type": "string"
|
||||
},
|
||||
"HookErrorInfo": {
|
||||
"properties": {
|
||||
"message": {
|
||||
"type": "string"
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"message",
|
||||
"path"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookEventName": {
|
||||
"enum": [
|
||||
"preToolUse",
|
||||
"permissionRequest",
|
||||
"postToolUse",
|
||||
"sessionStart",
|
||||
"userPromptSubmit",
|
||||
"stop"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookHandlerType": {
|
||||
"enum": [
|
||||
"command",
|
||||
"prompt",
|
||||
"agent"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookMetadata": {
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"eventName": {
|
||||
"$ref": "#/definitions/HookEventName"
|
||||
},
|
||||
"handlerType": {
|
||||
"$ref": "#/definitions/HookHandlerType"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"matcher": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"pluginId": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/HookSource"
|
||||
},
|
||||
"sourcePath": {
|
||||
"$ref": "#/definitions/AbsolutePathBuf"
|
||||
},
|
||||
"sourceRelativePath": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"statusMessage": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"timeoutSec": {
|
||||
"format": "uint64",
|
||||
"minimum": 0.0,
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"eventName",
|
||||
"handlerType",
|
||||
"key",
|
||||
"source",
|
||||
"sourcePath"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookSource": {
|
||||
"enum": [
|
||||
"system",
|
||||
"user",
|
||||
"project",
|
||||
"mdm",
|
||||
"sessionFlags",
|
||||
"plugin",
|
||||
"legacyManagedConfigFile",
|
||||
"legacyManagedConfigMdm",
|
||||
"unknown"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HooksListEntry": {
|
||||
"properties": {
|
||||
"cwd": {
|
||||
"type": "string"
|
||||
},
|
||||
"errors": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookErrorInfo"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"hooks": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookMetadata"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"cwd",
|
||||
"errors",
|
||||
"hooks"
|
||||
],
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/HooksListEntry"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "HooksListResponse",
|
||||
"type": "object"
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type HookConfigSource = "plugin";
|
||||
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type HookErrorInfo = { path: string, message: string, };
|
||||
@@ -0,0 +1,9 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { HookEventName } from "./HookEventName";
|
||||
import type { HookHandlerType } from "./HookHandlerType";
|
||||
import type { HookSource } from "./HookSource";
|
||||
|
||||
export type HookMetadata = { source: HookSource, pluginId: string | null, key: string, eventName: HookEventName, matcher: string | null, handlerType: HookHandlerType, command: string | null, timeoutSec: bigint | null, statusMessage: string | null, sourcePath: AbsolutePathBuf, sourceRelativePath: string | null, enabled: boolean, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HookConfigSource } from "./HookConfigSource";
|
||||
|
||||
export type HooksConfigWriteParams = { source: HookConfigSource, pluginId?: string | null, key: string, enabled: boolean, };
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type HooksConfigWriteResponse = { effectiveEnabled: boolean, };
|
||||
@@ -0,0 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HookErrorInfo } from "./HookErrorInfo";
|
||||
import type { HookMetadata } from "./HookMetadata";
|
||||
|
||||
export type HooksListEntry = { cwd: string, hooks: Array<HookMetadata>, errors: Array<HookErrorInfo>, };
|
||||
@@ -0,0 +1,9 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type HooksListParams = {
|
||||
/**
|
||||
* When omitted or empty, defaults to the current session working directory.
|
||||
*/
|
||||
cwds?: Array<string> | null, };
|
||||
@@ -0,0 +1,6 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { HooksListEntry } from "./HooksListEntry";
|
||||
|
||||
export type HooksListResponse = { data: Array<HooksListEntry>, };
|
||||
@@ -151,9 +151,12 @@ export type { GuardianRiskLevel } from "./GuardianRiskLevel";
|
||||
export type { GuardianUserAuthorization } from "./GuardianUserAuthorization";
|
||||
export type { GuardianWarningNotification } from "./GuardianWarningNotification";
|
||||
export type { HookCompletedNotification } from "./HookCompletedNotification";
|
||||
export type { HookConfigSource } from "./HookConfigSource";
|
||||
export type { HookErrorInfo } from "./HookErrorInfo";
|
||||
export type { HookEventName } from "./HookEventName";
|
||||
export type { HookExecutionMode } from "./HookExecutionMode";
|
||||
export type { HookHandlerType } from "./HookHandlerType";
|
||||
export type { HookMetadata } from "./HookMetadata";
|
||||
export type { HookOutputEntry } from "./HookOutputEntry";
|
||||
export type { HookOutputEntryKind } from "./HookOutputEntryKind";
|
||||
export type { HookPromptFragment } from "./HookPromptFragment";
|
||||
@@ -162,6 +165,11 @@ export type { HookRunSummary } from "./HookRunSummary";
|
||||
export type { HookScope } from "./HookScope";
|
||||
export type { HookSource } from "./HookSource";
|
||||
export type { HookStartedNotification } from "./HookStartedNotification";
|
||||
export type { HooksConfigWriteParams } from "./HooksConfigWriteParams";
|
||||
export type { HooksConfigWriteResponse } from "./HooksConfigWriteResponse";
|
||||
export type { HooksListEntry } from "./HooksListEntry";
|
||||
export type { HooksListParams } from "./HooksListParams";
|
||||
export type { HooksListResponse } from "./HooksListResponse";
|
||||
export type { ItemCompletedNotification } from "./ItemCompletedNotification";
|
||||
export type { ItemGuardianApprovalReviewCompletedNotification } from "./ItemGuardianApprovalReviewCompletedNotification";
|
||||
export type { ItemGuardianApprovalReviewStartedNotification } from "./ItemGuardianApprovalReviewStartedNotification";
|
||||
|
||||
@@ -440,6 +440,14 @@ client_request_definitions! {
|
||||
params: v2::SkillsConfigWriteParams,
|
||||
response: v2::SkillsConfigWriteResponse,
|
||||
},
|
||||
HooksList => "hooks/list" {
|
||||
params: v2::HooksListParams,
|
||||
response: v2::HooksListResponse,
|
||||
},
|
||||
HooksConfigWrite => "hooks/config/write" {
|
||||
params: v2::HooksConfigWriteParams,
|
||||
response: v2::HooksConfigWriteResponse,
|
||||
},
|
||||
PluginInstall => "plugin/install" {
|
||||
params: v2::PluginInstallParams,
|
||||
response: v2::PluginInstallResponse,
|
||||
|
||||
@@ -4612,6 +4612,83 @@ pub struct SkillsConfigWriteResponse {
|
||||
pub effective_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct HooksListParams {
|
||||
/// When omitted or empty, defaults to the current session working directory.
|
||||
#[ts(optional = nullable)]
|
||||
pub cwds: Option<Vec<PathBuf>>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct HooksListResponse {
|
||||
pub data: Vec<HooksListEntry>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct HooksListEntry {
|
||||
pub cwd: PathBuf,
|
||||
pub hooks: Vec<HookMetadata>,
|
||||
pub errors: Vec<HookErrorInfo>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct HookErrorInfo {
|
||||
pub path: PathBuf,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct HookMetadata {
|
||||
pub source: HookSource,
|
||||
pub plugin_id: Option<String>,
|
||||
pub key: String,
|
||||
pub event_name: HookEventName,
|
||||
pub matcher: Option<String>,
|
||||
pub handler_type: HookHandlerType,
|
||||
pub command: Option<String>,
|
||||
pub timeout_sec: Option<u64>,
|
||||
pub status_message: Option<String>,
|
||||
pub source_path: AbsolutePathBuf,
|
||||
pub source_relative_path: Option<String>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[ts(rename_all = "snake_case")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub enum HookConfigSource {
|
||||
Plugin,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct HooksConfigWriteParams {
|
||||
pub source: HookConfigSource,
|
||||
#[ts(optional = nullable)]
|
||||
pub plugin_id: Option<String>,
|
||||
pub key: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct HooksConfigWriteResponse {
|
||||
pub effective_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -40,6 +40,7 @@ codex-device-key = { workspace = true }
|
||||
codex-exec-server = { workspace = true }
|
||||
codex-features = { workspace = true }
|
||||
codex-git-utils = { workspace = true }
|
||||
codex-hooks = { workspace = true }
|
||||
codex-otel = { workspace = true }
|
||||
codex-shell-command = { workspace = true }
|
||||
codex-utils-cli = { workspace = true }
|
||||
|
||||
@@ -206,6 +206,8 @@ Example with notification opt-out:
|
||||
- `device/key/public` — return a device key's SPKI DER public key as base64 plus its `algorithm` and `protectionClass`.
|
||||
- `device/key/sign` — sign one of the accepted structured payload variants with a controller-local device key. The only accepted payload today is `remoteControlClientConnection`, which binds a server-issued `/client` websocket challenge to the enrolled controller device without signing the bearer token itself; this is intentionally not an arbitrary-byte signing API.
|
||||
- `skills/config/write` — write user-level skill config by name or absolute path.
|
||||
- `hooks/list` — list plugin-bundled hooks for one or more `cwd` values, including stable hook keys and effective per-hook enabled state from user/session config.
|
||||
- `hooks/config/write` — write user-level plugin hook enablement config by `pluginId` and stable hook `key`.
|
||||
- `plugin/install` — install a plugin from a discovered marketplace entry, rejecting marketplace entries marked unavailable for install, install MCPs if any, and return the effective plugin auth policy plus any apps that still need auth (**under development; do not call from production clients yet**).
|
||||
- `plugin/uninstall` — uninstall a plugin by id by removing its cached files and clearing its user-level config entry (**under development; do not call from production clients yet**).
|
||||
- `mcpServer/oauth/login` — start an OAuth login for a configured MCP server; returns an `authorization_url` and later emits `mcpServer/oauthLogin/completed` once the browser flow finishes.
|
||||
@@ -1450,6 +1452,33 @@ To enable or disable a skill by name:
|
||||
}
|
||||
```
|
||||
|
||||
To list plugin-bundled hooks:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "hooks/list",
|
||||
"id": 28,
|
||||
"params": {
|
||||
"cwds": ["/Users/alice/project"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To enable or disable a plugin-bundled hook:
|
||||
|
||||
```json
|
||||
{
|
||||
"method": "hooks/config/write",
|
||||
"id": 29,
|
||||
"params": {
|
||||
"source": "plugin",
|
||||
"pluginId": "demo-plugin@test-marketplace",
|
||||
"key": "hooks/hooks.json:PreToolUse:0:0",
|
||||
"enabled": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Apps
|
||||
|
||||
Use `app/list` to fetch available apps (connectors). Each entry includes metadata like the app `id`, display `name`, `installUrl`, `branding`, `appMetadata`, `labels`, whether it is currently accessible, and whether it is enabled in config.
|
||||
|
||||
@@ -75,6 +75,14 @@ use codex_app_server_protocol::GetConversationSummaryParams;
|
||||
use codex_app_server_protocol::GetConversationSummaryResponse;
|
||||
use codex_app_server_protocol::GitDiffToRemoteResponse;
|
||||
use codex_app_server_protocol::GitInfo as ApiGitInfo;
|
||||
use codex_app_server_protocol::HookConfigSource as ApiHookConfigSource;
|
||||
use codex_app_server_protocol::HookErrorInfo;
|
||||
use codex_app_server_protocol::HookMetadata;
|
||||
use codex_app_server_protocol::HooksConfigWriteParams;
|
||||
use codex_app_server_protocol::HooksConfigWriteResponse;
|
||||
use codex_app_server_protocol::HooksListEntry;
|
||||
use codex_app_server_protocol::HooksListParams;
|
||||
use codex_app_server_protocol::HooksListResponse;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::ListMcpServerStatusParams;
|
||||
use codex_app_server_protocol::ListMcpServerStatusResponse;
|
||||
@@ -296,6 +304,7 @@ use codex_feedback::CodexFeedback;
|
||||
use codex_feedback::FeedbackUploadOptions;
|
||||
use codex_git_utils::git_diff_to_remote;
|
||||
use codex_git_utils::resolve_root_git_project_for_trust;
|
||||
use codex_hooks::HookInventoryEntry;
|
||||
use codex_login::AuthManager;
|
||||
use codex_login::CLIENT_ID;
|
||||
use codex_login::CodexAuth;
|
||||
@@ -1042,6 +1051,10 @@ impl CodexMessageProcessor {
|
||||
self.skills_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::HooksList { request_id, params } => {
|
||||
self.hooks_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::MarketplaceAdd { request_id, params } => {
|
||||
self.marketplace_add(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
@@ -1070,6 +1083,10 @@ impl CodexMessageProcessor {
|
||||
self.skills_config_write(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::HooksConfigWrite { request_id, params } => {
|
||||
self.hooks_config_write(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::PluginInstall { request_id, params } => {
|
||||
self.plugin_install(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
@@ -6896,6 +6913,84 @@ impl CodexMessageProcessor {
|
||||
.send_response(request_id, SkillsListResponse { data })
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn hooks_list(&self, request_id: ConnectionRequestId, params: HooksListParams) {
|
||||
let mut cwds = params.cwds.unwrap_or_default();
|
||||
if cwds.is_empty() {
|
||||
cwds.push(self.config.cwd.to_path_buf());
|
||||
}
|
||||
|
||||
let auth = self.auth_manager.auth().await;
|
||||
let plugins_manager = self.thread_manager.plugins_manager();
|
||||
let mut data = Vec::new();
|
||||
for cwd in cwds {
|
||||
let cwd_abs = match AbsolutePathBuf::relative_to_current_dir(cwd.as_path()) {
|
||||
Ok(path) => path,
|
||||
Err(err) => {
|
||||
let error_path = cwd.clone();
|
||||
data.push(HooksListEntry {
|
||||
cwd,
|
||||
hooks: Vec::new(),
|
||||
errors: vec![HookErrorInfo {
|
||||
path: error_path,
|
||||
message: err.to_string(),
|
||||
}],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let config = match self
|
||||
.config_manager
|
||||
.load_latest_config(Some(cwd_abs.to_path_buf()))
|
||||
.await
|
||||
{
|
||||
Ok(config) => config,
|
||||
Err(err) => {
|
||||
let error_path = cwd.clone();
|
||||
data.push(HooksListEntry {
|
||||
cwd,
|
||||
hooks: Vec::new(),
|
||||
errors: vec![HookErrorInfo {
|
||||
path: error_path,
|
||||
message: err.to_string(),
|
||||
}],
|
||||
});
|
||||
continue;
|
||||
}
|
||||
};
|
||||
let workspace_codex_plugins_enabled = self
|
||||
.workspace_codex_plugins_enabled(&config, auth.as_ref())
|
||||
.await;
|
||||
let plugin_hook_sources = if config.features.enabled(Feature::PluginHooks)
|
||||
&& workspace_codex_plugins_enabled
|
||||
{
|
||||
plugins_manager
|
||||
.plugins_for_config(&config)
|
||||
.await
|
||||
.effective_plugin_hook_sources()
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
let hooks = codex_hooks::list_plugin_hooks(
|
||||
Some(&config.config_layer_stack),
|
||||
&plugin_hook_sources,
|
||||
)
|
||||
.into_iter()
|
||||
.map(hook_to_info)
|
||||
.collect();
|
||||
data.push(HooksListEntry {
|
||||
cwd,
|
||||
hooks,
|
||||
errors: Vec::new(),
|
||||
});
|
||||
}
|
||||
|
||||
self.outgoing
|
||||
.send_response(request_id, HooksListResponse { data })
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn marketplace_remove(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -7075,6 +7170,71 @@ impl CodexMessageProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
async fn hooks_config_write(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
params: HooksConfigWriteParams,
|
||||
) {
|
||||
let HooksConfigWriteParams {
|
||||
source,
|
||||
plugin_id,
|
||||
key,
|
||||
enabled,
|
||||
} = params;
|
||||
let edit = match source {
|
||||
ApiHookConfigSource::Plugin => {
|
||||
let Some(plugin_id) = plugin_id.filter(|plugin_id| !plugin_id.trim().is_empty())
|
||||
else {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
"hooks/config/write requires pluginId when source is plugin".to_string(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
};
|
||||
if key.trim().is_empty() {
|
||||
self.send_invalid_request_error(
|
||||
request_id,
|
||||
"hooks/config/write requires a non-empty key".to_string(),
|
||||
)
|
||||
.await;
|
||||
return;
|
||||
}
|
||||
ConfigEdit::SetHookConfig {
|
||||
plugin_id,
|
||||
key,
|
||||
enabled,
|
||||
}
|
||||
}
|
||||
};
|
||||
let result = ConfigEditsBuilder::new(&self.config.codex_home)
|
||||
.with_edits([edit])
|
||||
.apply()
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
self.thread_manager.plugins_manager().clear_cache();
|
||||
self.outgoing
|
||||
.send_response(
|
||||
request_id,
|
||||
HooksConfigWriteResponse {
|
||||
effective_enabled: enabled,
|
||||
},
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
let error = JSONRPCErrorError {
|
||||
code: INTERNAL_ERROR_CODE,
|
||||
message: format!("failed to write hooks config: {err}"),
|
||||
data: None,
|
||||
};
|
||||
self.outgoing.send_error(request_id, error).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn turn_start(
|
||||
&self,
|
||||
request_id: ConnectionRequestId,
|
||||
@@ -9387,6 +9547,23 @@ fn plugin_skills_to_info(
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn hook_to_info(hook: HookInventoryEntry) -> HookMetadata {
|
||||
HookMetadata {
|
||||
source: hook.source.into(),
|
||||
plugin_id: hook.plugin_id,
|
||||
key: hook.key,
|
||||
event_name: hook.event_name.into(),
|
||||
matcher: hook.matcher,
|
||||
handler_type: hook.handler_type.into(),
|
||||
command: hook.command,
|
||||
timeout_sec: hook.timeout_sec,
|
||||
status_message: hook.status_message,
|
||||
source_path: hook.source_path,
|
||||
source_relative_path: hook.source_relative_path,
|
||||
enabled: hook.enabled,
|
||||
}
|
||||
}
|
||||
|
||||
fn local_plugin_interface_to_info(interface: PluginManifestInterface) -> PluginInterface {
|
||||
PluginInterface {
|
||||
display_name: interface.display_name,
|
||||
|
||||
@@ -318,6 +318,7 @@ fn map_hooks_requirements_to_api(hooks: ManagedHooksRequirementsToml) -> Managed
|
||||
session_start,
|
||||
user_prompt_submit,
|
||||
stop,
|
||||
config: _,
|
||||
} = hooks;
|
||||
|
||||
ManagedHooksRequirements {
|
||||
|
||||
@@ -26,6 +26,8 @@ pub struct HookEventsToml {
|
||||
pub user_prompt_submit: Vec<MatcherGroup>,
|
||||
#[serde(rename = "Stop", default)]
|
||||
pub stop: Vec<MatcherGroup>,
|
||||
#[serde(default)]
|
||||
pub config: Vec<HookConfig>,
|
||||
}
|
||||
|
||||
impl HookEventsToml {
|
||||
@@ -37,6 +39,7 @@ impl HookEventsToml {
|
||||
session_start,
|
||||
user_prompt_submit,
|
||||
stop,
|
||||
config: _,
|
||||
} = self;
|
||||
pre_tool_use.is_empty()
|
||||
&& permission_request.is_empty()
|
||||
@@ -54,6 +57,7 @@ impl HookEventsToml {
|
||||
session_start,
|
||||
user_prompt_submit,
|
||||
stop,
|
||||
config: _,
|
||||
} = self;
|
||||
[
|
||||
pre_tool_use,
|
||||
@@ -81,6 +85,21 @@ impl HookEventsToml {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HookConfigSource {
|
||||
Plugin,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct HookConfig {
|
||||
pub source: HookConfigSource,
|
||||
#[serde(default)]
|
||||
pub plugin_id: Option<String>,
|
||||
pub key: String,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MatcherGroup {
|
||||
#[serde(default)]
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::HookConfig;
|
||||
use super::HookConfigSource;
|
||||
use super::HookEventsToml;
|
||||
use super::HookHandlerConfig;
|
||||
use super::HooksFile;
|
||||
@@ -81,6 +83,30 @@ statusMessage = "checking"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hook_events_deserialize_config_overrides() {
|
||||
let parsed: HookEventsToml = toml::from_str(
|
||||
r#"
|
||||
[[config]]
|
||||
source = "plugin"
|
||||
plugin_id = "openai-curated/superpowers"
|
||||
key = "hooks/hooks.json:SessionStart:0:0"
|
||||
enabled = false
|
||||
"#,
|
||||
)
|
||||
.expect("hook config TOML should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
parsed.config,
|
||||
vec![HookConfig {
|
||||
source: HookConfigSource::Plugin,
|
||||
plugin_id: Some("openai-curated/superpowers".to_string()),
|
||||
key: "hooks/hooks.json:SessionStart:0:0".to_string(),
|
||||
enabled: false,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_hooks_requirements_flatten_hook_events() {
|
||||
let parsed: ManagedHooksRequirementsToml = toml::from_str(
|
||||
|
||||
@@ -68,6 +68,8 @@ pub use diagnostics::format_config_error;
|
||||
pub use diagnostics::format_config_error_with_source;
|
||||
pub use diagnostics::io_error_from_config_error;
|
||||
pub use fingerprint::version_for_toml;
|
||||
pub use hook_config::HookConfig;
|
||||
pub use hook_config::HookConfigSource;
|
||||
pub use hook_config::HookEventsToml;
|
||||
pub use hook_config::HookHandlerConfig;
|
||||
pub use hook_config::HooksFile;
|
||||
|
||||
@@ -859,6 +859,35 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"HookConfig": {
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"plugin_id": {
|
||||
"default": null,
|
||||
"type": "string"
|
||||
},
|
||||
"source": {
|
||||
"$ref": "#/definitions/HookConfigSource"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"enabled",
|
||||
"key",
|
||||
"source"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"HookConfigSource": {
|
||||
"enum": [
|
||||
"plugin"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookEventsToml": {
|
||||
"properties": {
|
||||
"PermissionRequest": {
|
||||
@@ -902,6 +931,13 @@
|
||||
"$ref": "#/definitions/MatcherGroup"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"config": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookConfig"
|
||||
},
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
|
||||
@@ -61,6 +61,12 @@ pub enum ConfigEdit {
|
||||
SetSkillConfig { path: PathBuf, enabled: bool },
|
||||
/// Set or clear a skill config entry under `[[skills.config]]` by name.
|
||||
SetSkillConfigByName { name: String, enabled: bool },
|
||||
/// Set or clear a plugin hook config entry under `[[hooks.config]]`.
|
||||
SetHookConfig {
|
||||
plugin_id: String,
|
||||
key: String,
|
||||
enabled: bool,
|
||||
},
|
||||
/// Set trust_level under `[projects."<path>"]`,
|
||||
/// migrating inline tables to explicit tables.
|
||||
SetProjectTrustLevel { path: PathBuf, level: TrustLevel },
|
||||
@@ -79,6 +85,11 @@ enum SkillConfigSelector {
|
||||
Path(PathBuf),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
enum HookConfigSelector {
|
||||
Plugin { plugin_id: String, key: String },
|
||||
}
|
||||
|
||||
/// Produces a config edit that sets `[tui].theme = "<name>"`.
|
||||
pub fn syntax_theme_edit(name: &str) -> ConfigEdit {
|
||||
ConfigEdit::SetPath {
|
||||
@@ -519,6 +530,17 @@ impl ConfigDocument {
|
||||
ConfigEdit::SetSkillConfigByName { name, enabled } => {
|
||||
Ok(self.set_skill_config(SkillConfigSelector::Name(name.clone()), *enabled))
|
||||
}
|
||||
ConfigEdit::SetHookConfig {
|
||||
plugin_id,
|
||||
key,
|
||||
enabled,
|
||||
} => Ok(self.set_hook_config(
|
||||
HookConfigSelector::Plugin {
|
||||
plugin_id: plugin_id.clone(),
|
||||
key: key.clone(),
|
||||
},
|
||||
*enabled,
|
||||
)),
|
||||
ConfigEdit::SetPath { segments, value } => Ok(self.insert(segments, value.clone())),
|
||||
ConfigEdit::ClearPath { segments } => Ok(self.clear_owned(segments)),
|
||||
ConfigEdit::SetProjectTrustLevel { path, level } => {
|
||||
@@ -719,6 +741,122 @@ impl ConfigDocument {
|
||||
mutated
|
||||
}
|
||||
|
||||
fn set_hook_config(&mut self, selector: HookConfigSelector, enabled: bool) -> bool {
|
||||
let selector = match selector {
|
||||
HookConfigSelector::Plugin { plugin_id, key } => HookConfigSelector::Plugin {
|
||||
plugin_id: plugin_id.trim().to_string(),
|
||||
key: key.trim().to_string(),
|
||||
},
|
||||
};
|
||||
if matches!(
|
||||
&selector,
|
||||
HookConfigSelector::Plugin { plugin_id, key }
|
||||
if plugin_id.is_empty() || key.is_empty()
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut remove_hooks_table = false;
|
||||
let mut mutated = false;
|
||||
|
||||
{
|
||||
let root = self.doc.as_table_mut();
|
||||
let hooks_item = match root.get_mut("hooks") {
|
||||
Some(item) => item,
|
||||
None => {
|
||||
if enabled {
|
||||
return false;
|
||||
}
|
||||
root.insert(
|
||||
"hooks",
|
||||
TomlItem::Table(document_helpers::new_implicit_table()),
|
||||
);
|
||||
let Some(item) = root.get_mut("hooks") else {
|
||||
return false;
|
||||
};
|
||||
item
|
||||
}
|
||||
};
|
||||
|
||||
if document_helpers::ensure_table_for_write(hooks_item).is_none() {
|
||||
if enabled {
|
||||
return false;
|
||||
}
|
||||
*hooks_item = TomlItem::Table(document_helpers::new_implicit_table());
|
||||
}
|
||||
let Some(hooks_table) = hooks_item.as_table_mut() else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let config_item = match hooks_table.get_mut("config") {
|
||||
Some(item) => item,
|
||||
None => {
|
||||
if enabled {
|
||||
return false;
|
||||
}
|
||||
hooks_table.insert("config", TomlItem::ArrayOfTables(ArrayOfTables::new()));
|
||||
let Some(item) = hooks_table.get_mut("config") else {
|
||||
return false;
|
||||
};
|
||||
item
|
||||
}
|
||||
};
|
||||
|
||||
if !matches!(config_item, TomlItem::ArrayOfTables(_)) {
|
||||
if enabled {
|
||||
return false;
|
||||
}
|
||||
*config_item = TomlItem::ArrayOfTables(ArrayOfTables::new());
|
||||
}
|
||||
|
||||
let TomlItem::ArrayOfTables(overrides) = config_item else {
|
||||
return false;
|
||||
};
|
||||
|
||||
let existing_index = overrides.iter().enumerate().find_map(|(idx, table)| {
|
||||
hook_config_selector_from_table(table)
|
||||
.filter(|value| value == &selector)
|
||||
.map(|_| idx)
|
||||
});
|
||||
|
||||
if enabled {
|
||||
if let Some(index) = existing_index {
|
||||
overrides.remove(index);
|
||||
mutated = true;
|
||||
if overrides.is_empty() {
|
||||
hooks_table.remove("config");
|
||||
if hooks_table.is_empty() {
|
||||
remove_hooks_table = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some(index) = existing_index {
|
||||
for (idx, table) in overrides.iter_mut().enumerate() {
|
||||
if idx == index {
|
||||
write_hook_config_selector(table, &selector);
|
||||
table["enabled"] = value(false);
|
||||
mutated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut entry = TomlTable::new();
|
||||
entry.set_implicit(false);
|
||||
write_hook_config_selector(&mut entry, &selector);
|
||||
entry["enabled"] = value(false);
|
||||
overrides.push(entry);
|
||||
mutated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if remove_hooks_table {
|
||||
let root = self.doc.as_table_mut();
|
||||
root.remove("hooks");
|
||||
}
|
||||
|
||||
mutated
|
||||
}
|
||||
|
||||
fn scoped_segments(&self, scope: Scope, segments: &[&str]) -> Vec<String> {
|
||||
let resolved: Vec<String> = segments
|
||||
.iter()
|
||||
@@ -865,6 +1003,41 @@ fn write_skill_config_selector(table: &mut TomlTable, selector: &SkillConfigSele
|
||||
}
|
||||
}
|
||||
|
||||
fn hook_config_selector_from_table(table: &TomlTable) -> Option<HookConfigSelector> {
|
||||
let source = table
|
||||
.get("source")
|
||||
.and_then(|item| item.as_str())
|
||||
.map(str::trim);
|
||||
let plugin_id = table
|
||||
.get("plugin_id")
|
||||
.and_then(|item| item.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|plugin_id| !plugin_id.is_empty());
|
||||
let key = table
|
||||
.get("key")
|
||||
.and_then(|item| item.as_str())
|
||||
.map(str::trim)
|
||||
.filter(|key| !key.is_empty());
|
||||
|
||||
match (source, plugin_id, key) {
|
||||
(Some("plugin"), Some(plugin_id), Some(key)) => Some(HookConfigSelector::Plugin {
|
||||
plugin_id: plugin_id.to_string(),
|
||||
key: key.to_string(),
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn write_hook_config_selector(table: &mut TomlTable, selector: &HookConfigSelector) {
|
||||
match selector {
|
||||
HookConfigSelector::Plugin { plugin_id, key } => {
|
||||
table["source"] = value("plugin");
|
||||
table["plugin_id"] = value(plugin_id.clone());
|
||||
table["key"] = value(key.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Persist edits using a blocking strategy.
|
||||
pub fn apply_blocking(
|
||||
codex_home: &Path,
|
||||
|
||||
@@ -133,6 +133,58 @@ enabled = false
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_hook_config_writes_disabled_plugin_entry() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.with_edits([ConfigEdit::SetHookConfig {
|
||||
plugin_id: "demo-plugin@test-marketplace".to_string(),
|
||||
key: "hooks/hooks.json:PreToolUse:0:0".to_string(),
|
||||
enabled: false,
|
||||
}])
|
||||
.apply_blocking()
|
||||
.expect("persist");
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
let expected = r#"[[hooks.config]]
|
||||
source = "plugin"
|
||||
plugin_id = "demo-plugin@test-marketplace"
|
||||
key = "hooks/hooks.json:PreToolUse:0:0"
|
||||
enabled = false
|
||||
"#;
|
||||
assert_eq!(contents, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn set_hook_config_removes_entry_when_enabled() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
let codex_home = tmp.path();
|
||||
std::fs::write(
|
||||
codex_home.join(CONFIG_TOML_FILE),
|
||||
r#"[[hooks.config]]
|
||||
source = "plugin"
|
||||
plugin_id = "demo-plugin@test-marketplace"
|
||||
key = "hooks/hooks.json:PreToolUse:0:0"
|
||||
enabled = false
|
||||
"#,
|
||||
)
|
||||
.expect("seed config");
|
||||
|
||||
ConfigEditsBuilder::new(codex_home)
|
||||
.with_edits([ConfigEdit::SetHookConfig {
|
||||
plugin_id: "demo-plugin@test-marketplace".to_string(),
|
||||
key: "hooks/hooks.json:PreToolUse:0:0".to_string(),
|
||||
enabled: true,
|
||||
}])
|
||||
.apply_blocking()
|
||||
.expect("persist");
|
||||
|
||||
let contents = std::fs::read_to_string(codex_home.join(CONFIG_TOML_FILE)).expect("read config");
|
||||
assert_eq!(contents, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocking_set_model_preserves_inline_table_contents() {
|
||||
let tmp = tempdir().expect("tmpdir");
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use codex_config::ConfigLayerSource;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::HookConfig;
|
||||
use codex_config::HookConfigSource;
|
||||
use codex_config::HookEventsToml;
|
||||
use codex_protocol::protocol::HookEventName;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct HookConfigRules {
|
||||
plugin: HashMap<(String, String), bool>,
|
||||
}
|
||||
|
||||
impl HookConfigRules {
|
||||
pub(crate) fn from_stack(
|
||||
config_layer_stack: &ConfigLayerStack,
|
||||
warnings: &mut Vec<String>,
|
||||
) -> Self {
|
||||
let mut rules = Self::default();
|
||||
for layer in config_layer_stack.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ true,
|
||||
) {
|
||||
if !matches!(
|
||||
layer.name,
|
||||
ConfigLayerSource::User { .. } | ConfigLayerSource::SessionFlags
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let Some(hooks_value) = layer.config.get("hooks") else {
|
||||
continue;
|
||||
};
|
||||
let hooks: HookEventsToml = match hooks_value.clone().try_into() {
|
||||
Ok(hooks) => hooks,
|
||||
Err(err) => {
|
||||
warnings.push(format!("failed to parse TOML hooks config: {err}"));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
for entry in hooks.config {
|
||||
rules.append(entry, warnings);
|
||||
}
|
||||
}
|
||||
|
||||
rules
|
||||
}
|
||||
|
||||
pub(crate) fn enabled_for_plugin_hook(&self, plugin_id: &str, key: &str) -> bool {
|
||||
self.plugin
|
||||
.get(&(plugin_id.to_string(), key.to_string()))
|
||||
.copied()
|
||||
.unwrap_or({
|
||||
// TODO(abhinav): Default-enabled plugin hooks are temporary until hook trust is added.
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
fn append(&mut self, entry: HookConfig, warnings: &mut Vec<String>) {
|
||||
match entry.source {
|
||||
HookConfigSource::Plugin => {
|
||||
let Some(plugin_id) = entry.plugin_id else {
|
||||
warnings.push(
|
||||
"ignoring plugin hooks.config entry without a plugin_id selector"
|
||||
.to_string(),
|
||||
);
|
||||
return;
|
||||
};
|
||||
if plugin_id.trim().is_empty() {
|
||||
warnings.push(
|
||||
"ignoring plugin hooks.config entry with empty plugin_id".to_string(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
if entry.key.trim().is_empty() {
|
||||
warnings.push("ignoring hooks.config entry with empty key".to_string());
|
||||
return;
|
||||
}
|
||||
self.plugin.insert((plugin_id, entry.key), entry.enabled);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn hook_config_key(
|
||||
source_relative_path: &str,
|
||||
event_name: HookEventName,
|
||||
group_index: usize,
|
||||
handler_index: usize,
|
||||
) -> String {
|
||||
format!(
|
||||
"{}:{}:{}:{}",
|
||||
source_relative_path,
|
||||
hook_event_name_config_label(event_name),
|
||||
group_index,
|
||||
handler_index
|
||||
)
|
||||
}
|
||||
|
||||
fn hook_event_name_config_label(event_name: HookEventName) -> &'static str {
|
||||
match event_name {
|
||||
HookEventName::PreToolUse => "PreToolUse",
|
||||
HookEventName::PermissionRequest => "PermissionRequest",
|
||||
HookEventName::PostToolUse => "PostToolUse",
|
||||
HookEventName::SessionStart => "SessionStart",
|
||||
HookEventName::UserPromptSubmit => "UserPromptSubmit",
|
||||
HookEventName::Stop => "Stop",
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,8 @@ use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::ConfiguredHandler;
|
||||
use super::config_rules::HookConfigRules;
|
||||
use super::config_rules::hook_config_key;
|
||||
use crate::events::common::matcher_pattern_for_event;
|
||||
use crate::events::common::validate_matcher_pattern;
|
||||
use codex_protocol::protocol::HookSource;
|
||||
@@ -33,6 +35,20 @@ struct HookHandlerSource<'a> {
|
||||
is_managed: bool,
|
||||
source: HookSource,
|
||||
env: HashMap<String, String>,
|
||||
plugin: Option<PluginHandlerSource>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct PluginHandlerSource {
|
||||
plugin_id: String,
|
||||
source_relative_path: String,
|
||||
}
|
||||
|
||||
struct AppendGroupHandlersContext<'a, 'b> {
|
||||
handlers: &'a mut Vec<ConfiguredHandler>,
|
||||
warnings: &'a mut Vec<String>,
|
||||
display_order: &'a mut i64,
|
||||
hook_config_rules: &'b HookConfigRules,
|
||||
}
|
||||
|
||||
pub(crate) fn discover_handlers(
|
||||
@@ -48,6 +64,7 @@ pub(crate) fn discover_handlers(
|
||||
&mut warnings,
|
||||
&mut display_order,
|
||||
plugin_hook_sources,
|
||||
&HookConfigRules::default(),
|
||||
);
|
||||
return DiscoveryResult { handlers, warnings };
|
||||
};
|
||||
@@ -55,6 +72,7 @@ pub(crate) fn discover_handlers(
|
||||
let mut handlers = Vec::new();
|
||||
let mut warnings = Vec::new();
|
||||
let mut display_order = 0_i64;
|
||||
let hook_config_rules = HookConfigRules::from_stack(config_layer_stack, &mut warnings);
|
||||
|
||||
append_managed_requirement_handlers(
|
||||
&mut handlers,
|
||||
@@ -93,8 +111,10 @@ pub(crate) fn discover_handlers(
|
||||
is_managed: false,
|
||||
source: hook_source,
|
||||
env: HashMap::new(),
|
||||
plugin: None,
|
||||
},
|
||||
hook_events,
|
||||
&hook_config_rules,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -108,8 +128,10 @@ pub(crate) fn discover_handlers(
|
||||
is_managed: false,
|
||||
source: hook_source,
|
||||
env: HashMap::new(),
|
||||
plugin: None,
|
||||
},
|
||||
hook_events,
|
||||
&hook_config_rules,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -119,6 +141,7 @@ pub(crate) fn discover_handlers(
|
||||
&mut warnings,
|
||||
&mut display_order,
|
||||
plugin_hook_sources,
|
||||
&hook_config_rules,
|
||||
);
|
||||
|
||||
DiscoveryResult { handlers, warnings }
|
||||
@@ -147,8 +170,10 @@ fn append_managed_requirement_handlers(
|
||||
is_managed: true,
|
||||
source: hook_source_for_requirement_source(managed_hooks.source.as_ref()),
|
||||
env: HashMap::new(),
|
||||
plugin: None,
|
||||
},
|
||||
managed_hooks.get().hooks.clone(),
|
||||
&HookConfigRules::default(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -157,14 +182,15 @@ fn append_plugin_hook_sources(
|
||||
warnings: &mut Vec<String>,
|
||||
display_order: &mut i64,
|
||||
plugin_hook_sources: Vec<PluginHookSource>,
|
||||
hook_config_rules: &HookConfigRules,
|
||||
) {
|
||||
// TODO(abhinav): check enabled/trusted state here before plugin hooks become runnable.
|
||||
for source in plugin_hook_sources {
|
||||
let PluginHookSource {
|
||||
plugin_id,
|
||||
plugin_root,
|
||||
source_path,
|
||||
source_relative_path,
|
||||
hooks,
|
||||
..
|
||||
} = source;
|
||||
let mut env = HashMap::new();
|
||||
let plugin_root_value = plugin_root.display().to_string();
|
||||
@@ -180,8 +206,13 @@ fn append_plugin_hook_sources(
|
||||
is_managed: false,
|
||||
source: HookSource::Plugin,
|
||||
env,
|
||||
plugin: Some(PluginHandlerSource {
|
||||
plugin_id: plugin_id.as_key(),
|
||||
source_relative_path,
|
||||
}),
|
||||
},
|
||||
hooks,
|
||||
hook_config_rules,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -328,6 +359,7 @@ fn append_hook_events(
|
||||
display_order: &mut i64,
|
||||
source: HookHandlerSource<'_>,
|
||||
hook_events: HookEventsToml,
|
||||
hook_config_rules: &HookConfigRules,
|
||||
) {
|
||||
for (event_name, groups) in hook_events.into_matcher_groups() {
|
||||
append_matcher_groups(
|
||||
@@ -337,6 +369,7 @@ fn append_hook_events(
|
||||
source.clone(),
|
||||
event_name,
|
||||
groups,
|
||||
hook_config_rules,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -348,14 +381,20 @@ fn append_matcher_groups(
|
||||
source: HookHandlerSource<'_>,
|
||||
event_name: codex_protocol::protocol::HookEventName,
|
||||
groups: Vec<MatcherGroup>,
|
||||
hook_config_rules: &HookConfigRules,
|
||||
) {
|
||||
for group in groups {
|
||||
let mut context = AppendGroupHandlersContext {
|
||||
handlers,
|
||||
warnings,
|
||||
display_order,
|
||||
hook_config_rules,
|
||||
};
|
||||
for (group_index, group) in groups.into_iter().enumerate() {
|
||||
append_group_handlers(
|
||||
handlers,
|
||||
warnings,
|
||||
display_order,
|
||||
&mut context,
|
||||
source.clone(),
|
||||
event_name,
|
||||
group_index,
|
||||
matcher_pattern_for_event(event_name, group.matcher.as_deref()),
|
||||
group.hooks,
|
||||
);
|
||||
@@ -363,25 +402,39 @@ fn append_matcher_groups(
|
||||
}
|
||||
|
||||
fn append_group_handlers(
|
||||
handlers: &mut Vec<ConfiguredHandler>,
|
||||
warnings: &mut Vec<String>,
|
||||
display_order: &mut i64,
|
||||
context: &mut AppendGroupHandlersContext<'_, '_>,
|
||||
source: HookHandlerSource<'_>,
|
||||
event_name: codex_protocol::protocol::HookEventName,
|
||||
group_index: usize,
|
||||
matcher: Option<&str>,
|
||||
group_handlers: Vec<HookHandlerConfig>,
|
||||
) {
|
||||
if let Some(matcher) = matcher
|
||||
&& let Err(err) = validate_matcher_pattern(matcher)
|
||||
{
|
||||
warnings.push(format!(
|
||||
context.warnings.push(format!(
|
||||
"invalid matcher {matcher:?} in {}: {err}",
|
||||
source.path.display()
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
for handler in group_handlers {
|
||||
for (handler_index, handler) in group_handlers.into_iter().enumerate() {
|
||||
if let Some(plugin) = &source.plugin {
|
||||
let key = hook_config_key(
|
||||
&plugin.source_relative_path,
|
||||
event_name,
|
||||
group_index,
|
||||
handler_index,
|
||||
);
|
||||
if !context
|
||||
.hook_config_rules
|
||||
.enabled_for_plugin_hook(&plugin.plugin_id, &key)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
match handler {
|
||||
HookHandlerConfig::Command {
|
||||
command,
|
||||
@@ -390,21 +443,21 @@ fn append_group_handlers(
|
||||
status_message,
|
||||
} => {
|
||||
if r#async {
|
||||
warnings.push(format!(
|
||||
context.warnings.push(format!(
|
||||
"skipping async hook in {}: async hooks are not supported yet",
|
||||
source.path.display()
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if command.trim().is_empty() {
|
||||
warnings.push(format!(
|
||||
context.warnings.push(format!(
|
||||
"skipping empty hook command in {}",
|
||||
source.path.display()
|
||||
));
|
||||
continue;
|
||||
}
|
||||
let timeout_sec = timeout_sec.unwrap_or(600).max(1);
|
||||
handlers.push(ConfiguredHandler {
|
||||
context.handlers.push(ConfiguredHandler {
|
||||
event_name,
|
||||
is_managed: source.is_managed,
|
||||
matcher: matcher.map(ToOwned::to_owned),
|
||||
@@ -413,16 +466,16 @@ fn append_group_handlers(
|
||||
status_message,
|
||||
source_path: source.path.clone(),
|
||||
source: source.source,
|
||||
display_order: *display_order,
|
||||
display_order: *context.display_order,
|
||||
env: source.env.clone(),
|
||||
});
|
||||
*display_order += 1;
|
||||
*context.display_order += 1;
|
||||
}
|
||||
HookHandlerConfig::Prompt {} => warnings.push(format!(
|
||||
HookHandlerConfig::Prompt {} => context.warnings.push(format!(
|
||||
"skipping prompt hook in {}: prompt hooks are not supported yet",
|
||||
source.path.display()
|
||||
)),
|
||||
HookHandlerConfig::Agent {} => warnings.push(format!(
|
||||
HookHandlerConfig::Agent {} => context.warnings.push(format!(
|
||||
"skipping agent hook in {}: agent hooks are not supported yet",
|
||||
source.path.display()
|
||||
)),
|
||||
@@ -489,9 +542,14 @@ mod tests {
|
||||
is_managed: false,
|
||||
source: hook_source(),
|
||||
env: std::collections::HashMap::new(),
|
||||
plugin: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn hook_config_rules() -> crate::engine::config_rules::HookConfigRules {
|
||||
crate::engine::config_rules::HookConfigRules::default()
|
||||
}
|
||||
|
||||
fn command_group(matcher: Option<&str>) -> MatcherGroup {
|
||||
MatcherGroup {
|
||||
matcher: matcher.map(str::to_string),
|
||||
@@ -518,6 +576,7 @@ mod tests {
|
||||
hook_handler_source(&source_path),
|
||||
HookEventName::UserPromptSubmit,
|
||||
vec![command_group(Some("["))],
|
||||
&hook_config_rules(),
|
||||
);
|
||||
|
||||
assert_eq!(warnings, Vec::<String>::new());
|
||||
@@ -552,6 +611,7 @@ mod tests {
|
||||
hook_handler_source(&source_path),
|
||||
HookEventName::PreToolUse,
|
||||
vec![command_group(Some("^Bash$"))],
|
||||
&hook_config_rules(),
|
||||
);
|
||||
|
||||
assert_eq!(warnings, Vec::<String>::new());
|
||||
@@ -586,6 +646,7 @@ mod tests {
|
||||
hook_handler_source(&source_path),
|
||||
HookEventName::PreToolUse,
|
||||
vec![command_group(Some("*"))],
|
||||
&hook_config_rules(),
|
||||
);
|
||||
|
||||
assert_eq!(warnings, Vec::<String>::new());
|
||||
@@ -607,6 +668,7 @@ mod tests {
|
||||
hook_handler_source(&source_path),
|
||||
HookEventName::PostToolUse,
|
||||
vec![command_group(Some("Edit|Write"))],
|
||||
&hook_config_rules(),
|
||||
);
|
||||
|
||||
assert_eq!(warnings, Vec::<String>::new());
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::HookHandlerConfig;
|
||||
use codex_plugin::PluginHookSource;
|
||||
use codex_protocol::protocol::HookEventName;
|
||||
use codex_protocol::protocol::HookHandlerType;
|
||||
use codex_protocol::protocol::HookSource;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
|
||||
use super::config_rules::HookConfigRules;
|
||||
use super::config_rules::hook_config_key;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct HookInventoryEntry {
|
||||
pub source: HookSource,
|
||||
pub plugin_id: Option<String>,
|
||||
pub key: String,
|
||||
pub event_name: HookEventName,
|
||||
pub matcher: Option<String>,
|
||||
pub handler_type: HookHandlerType,
|
||||
pub command: Option<String>,
|
||||
pub timeout_sec: Option<u64>,
|
||||
pub status_message: Option<String>,
|
||||
pub source_path: AbsolutePathBuf,
|
||||
pub source_relative_path: Option<String>,
|
||||
pub enabled: bool,
|
||||
}
|
||||
|
||||
pub fn list_plugin_hooks(
|
||||
config_layer_stack: Option<&ConfigLayerStack>,
|
||||
plugin_hook_sources: &[PluginHookSource],
|
||||
) -> Vec<HookInventoryEntry> {
|
||||
let mut warnings = Vec::new();
|
||||
let hook_config_rules = config_layer_stack
|
||||
.map(|config_layer_stack| HookConfigRules::from_stack(config_layer_stack, &mut warnings))
|
||||
.unwrap_or_default();
|
||||
let mut entries = Vec::new();
|
||||
|
||||
for source in plugin_hook_sources {
|
||||
let plugin_id = source.plugin_id.as_key();
|
||||
for (event_name, groups) in source.hooks.clone().into_matcher_groups() {
|
||||
for (group_index, group) in groups.into_iter().enumerate() {
|
||||
for (handler_index, handler) in group.hooks.into_iter().enumerate() {
|
||||
let key = hook_config_key(
|
||||
&source.source_relative_path,
|
||||
event_name,
|
||||
group_index,
|
||||
handler_index,
|
||||
);
|
||||
let enabled = hook_config_rules.enabled_for_plugin_hook(&plugin_id, &key);
|
||||
let (handler_type, command, timeout_sec, status_message) =
|
||||
hook_inventory_handler_fields(handler);
|
||||
entries.push(HookInventoryEntry {
|
||||
source: HookSource::Plugin,
|
||||
plugin_id: Some(plugin_id.clone()),
|
||||
key,
|
||||
event_name,
|
||||
matcher: group.matcher.clone(),
|
||||
handler_type,
|
||||
command,
|
||||
timeout_sec,
|
||||
status_message,
|
||||
source_path: source.source_path.clone(),
|
||||
source_relative_path: Some(source.source_relative_path.clone()),
|
||||
enabled,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
fn hook_inventory_handler_fields(
|
||||
handler: HookHandlerConfig,
|
||||
) -> (HookHandlerType, Option<String>, Option<u64>, Option<String>) {
|
||||
match handler {
|
||||
HookHandlerConfig::Command {
|
||||
command,
|
||||
timeout_sec,
|
||||
r#async: _,
|
||||
status_message,
|
||||
} => (
|
||||
HookHandlerType::Command,
|
||||
Some(command),
|
||||
timeout_sec,
|
||||
status_message,
|
||||
),
|
||||
HookHandlerConfig::Prompt {} => (HookHandlerType::Prompt, None, None, None),
|
||||
HookHandlerConfig::Agent {} => (HookHandlerType::Agent, None, None, None),
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
pub(crate) mod command_runner;
|
||||
pub(crate) mod config_rules;
|
||||
pub(crate) mod discovery;
|
||||
pub(crate) mod dispatcher;
|
||||
pub(crate) mod inventory;
|
||||
pub(crate) mod output_parser;
|
||||
pub(crate) mod schema_loader;
|
||||
|
||||
|
||||
@@ -430,3 +430,93 @@ Path(r"{log_path}").write_text(json.dumps({{
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plugin_hook_sources_can_be_disabled_by_user_config() {
|
||||
let temp = tempdir().expect("create temp dir");
|
||||
let plugin_root =
|
||||
AbsolutePathBuf::try_from(temp.path().join("demo-plugin")).expect("plugin root");
|
||||
let source_path = plugin_root.join("hooks/hooks.json");
|
||||
let plugin_id = PluginId::parse("demo-plugin@test-marketplace").expect("plugin id");
|
||||
let plugin_hook_sources = vec![PluginHookSource {
|
||||
plugin_id,
|
||||
plugin_root,
|
||||
source_path,
|
||||
source_relative_path: "hooks/hooks.json".to_string(),
|
||||
hooks: HookEventsToml {
|
||||
pre_tool_use: vec![MatcherGroup {
|
||||
matcher: Some("Bash".to_string()),
|
||||
hooks: vec![HookHandlerConfig::Command {
|
||||
command: "echo plugin".to_string(),
|
||||
timeout_sec: Some(5),
|
||||
r#async: false,
|
||||
status_message: None,
|
||||
}],
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
}];
|
||||
let mut config_toml = TomlValue::Table(Default::default());
|
||||
let TomlValue::Table(config_table) = &mut config_toml else {
|
||||
unreachable!("config TOML root should be a table");
|
||||
};
|
||||
let mut hooks_table = TomlValue::Table(Default::default());
|
||||
let TomlValue::Table(hooks_entries) = &mut hooks_table else {
|
||||
unreachable!("hooks entry should be a table");
|
||||
};
|
||||
let mut config_entry = TomlValue::Table(Default::default());
|
||||
let TomlValue::Table(config_entry_table) = &mut config_entry else {
|
||||
unreachable!("hooks config entry should be a table");
|
||||
};
|
||||
config_entry_table.insert(
|
||||
"source".to_string(),
|
||||
TomlValue::String("plugin".to_string()),
|
||||
);
|
||||
config_entry_table.insert(
|
||||
"plugin_id".to_string(),
|
||||
TomlValue::String("demo-plugin@test-marketplace".to_string()),
|
||||
);
|
||||
config_entry_table.insert(
|
||||
"key".to_string(),
|
||||
TomlValue::String("hooks/hooks.json:PreToolUse:0:0".to_string()),
|
||||
);
|
||||
config_entry_table.insert("enabled".to_string(), TomlValue::Boolean(false));
|
||||
hooks_entries.insert("config".to_string(), TomlValue::Array(vec![config_entry]));
|
||||
config_table.insert("hooks".to_string(), hooks_table);
|
||||
let config_path =
|
||||
AbsolutePathBuf::try_from(temp.path().join("config.toml")).expect("absolute config path");
|
||||
let config_layer_stack = ConfigLayerStack::new(
|
||||
vec![ConfigLayerEntry::new(
|
||||
ConfigLayerSource::User { file: config_path },
|
||||
config_toml,
|
||||
)],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)
|
||||
.expect("config layer stack");
|
||||
|
||||
let engine = ClaudeHooksEngine::new(
|
||||
/*enabled*/ true,
|
||||
Some(&config_layer_stack),
|
||||
plugin_hook_sources,
|
||||
CommandShell {
|
||||
program: String::new(),
|
||||
args: Vec::new(),
|
||||
},
|
||||
);
|
||||
|
||||
let preview = engine.preview_pre_tool_use(&PreToolUseRequest {
|
||||
session_id: ThreadId::new(),
|
||||
turn_id: "turn-1".to_string(),
|
||||
cwd: cwd(),
|
||||
transcript_path: None,
|
||||
model: "gpt-test".to_string(),
|
||||
permission_mode: "default".to_string(),
|
||||
tool_name: "Bash".to_string(),
|
||||
matcher_aliases: Vec::new(),
|
||||
tool_use_id: "tool-1".to_string(),
|
||||
tool_input: serde_json::json!({ "command": "echo hello" }),
|
||||
});
|
||||
|
||||
assert_eq!(preview, Vec::new());
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ mod registry;
|
||||
mod schema;
|
||||
mod types;
|
||||
|
||||
pub use engine::inventory::HookInventoryEntry;
|
||||
pub use engine::inventory::list_plugin_hooks;
|
||||
pub use events::permission_request::PermissionRequestDecision;
|
||||
pub use events::permission_request::PermissionRequestOutcome;
|
||||
pub use events::permission_request::PermissionRequestRequest;
|
||||
|
||||
Reference in New Issue
Block a user