mirror of
https://github.com/openai/codex.git
synced 2026-04-14 01:35:00 +00:00
Compare commits
1 Commits
dev/shaqay
...
dev/mzeng/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da838ef284 |
@@ -946,6 +946,28 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"InboxListParams": {
|
||||
"description": "List tracked inbox entries from `$CODEX_HOME/inbox`.",
|
||||
"type": "object"
|
||||
},
|
||||
"InboxUpdateParams": {
|
||||
"description": "Update user-owned inbox tracking state for one record.",
|
||||
"properties": {
|
||||
"lastReadAt": {
|
||||
"description": "RFC 3339 timestamp to store in `tracking.last_read_at`.",
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"description": "Exact inbox filename under `$CODEX_HOME/inbox`, for example `thread__20260319T170500Z__slack__C01234567__1712345678.000000.json`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"lastReadAt",
|
||||
"threadId"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"InitializeCapabilities": {
|
||||
"description": "Client-declared capabilities negotiated during initialize.",
|
||||
"properties": {
|
||||
@@ -3818,6 +3840,54 @@
|
||||
"title": "App/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"inbox/list"
|
||||
],
|
||||
"title": "Inbox/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/InboxListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Inbox/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"inbox/update"
|
||||
],
|
||||
"title": "Inbox/updateRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/InboxUpdateParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Inbox/updateRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
|
||||
@@ -715,6 +715,54 @@
|
||||
"title": "App/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"inbox/list"
|
||||
],
|
||||
"title": "Inbox/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/InboxListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Inbox/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/v2/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"inbox/update"
|
||||
],
|
||||
"title": "Inbox/updateRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/v2/InboxUpdateParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Inbox/updateRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -8131,6 +8179,58 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"InboxListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "List tracked inbox entries from `$CODEX_HOME/inbox`.",
|
||||
"title": "InboxListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"InboxListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Tracked inbox records are returned as the on-disk JSON shape defined by the sort-inbox skill's thread record format.",
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "InboxListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"InboxUpdateParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Update user-owned inbox tracking state for one record.",
|
||||
"properties": {
|
||||
"lastReadAt": {
|
||||
"description": "RFC 3339 timestamp to store in `tracking.last_read_at`.",
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"description": "Exact inbox filename under `$CODEX_HOME/inbox`, for example `thread__20260319T170500Z__slack__C01234567__1712345678.000000.json`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"lastReadAt",
|
||||
"threadId"
|
||||
],
|
||||
"title": "InboxUpdateParams",
|
||||
"type": "object"
|
||||
},
|
||||
"InboxUpdateResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"entry": true
|
||||
},
|
||||
"required": [
|
||||
"entry"
|
||||
],
|
||||
"title": "InboxUpdateResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"InputModality": {
|
||||
"description": "Canonical user-input modality tags advertised by a model.",
|
||||
"oneOf": [
|
||||
|
||||
@@ -1246,6 +1246,54 @@
|
||||
"title": "App/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"inbox/list"
|
||||
],
|
||||
"title": "Inbox/listRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/InboxListParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Inbox/listRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
"$ref": "#/definitions/RequestId"
|
||||
},
|
||||
"method": {
|
||||
"enum": [
|
||||
"inbox/update"
|
||||
],
|
||||
"title": "Inbox/updateRequestMethod",
|
||||
"type": "string"
|
||||
},
|
||||
"params": {
|
||||
"$ref": "#/definitions/InboxUpdateParams"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"method",
|
||||
"params"
|
||||
],
|
||||
"title": "Inbox/updateRequest",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"properties": {
|
||||
"id": {
|
||||
@@ -4855,6 +4903,58 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"InboxListParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "List tracked inbox entries from `$CODEX_HOME/inbox`.",
|
||||
"title": "InboxListParams",
|
||||
"type": "object"
|
||||
},
|
||||
"InboxListResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Tracked inbox records are returned as the on-disk JSON shape defined by the sort-inbox skill's thread record format.",
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "InboxListResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"InboxUpdateParams": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Update user-owned inbox tracking state for one record.",
|
||||
"properties": {
|
||||
"lastReadAt": {
|
||||
"description": "RFC 3339 timestamp to store in `tracking.last_read_at`.",
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"description": "Exact inbox filename under `$CODEX_HOME/inbox`, for example `thread__20260319T170500Z__slack__C01234567__1712345678.000000.json`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"lastReadAt",
|
||||
"threadId"
|
||||
],
|
||||
"title": "InboxUpdateParams",
|
||||
"type": "object"
|
||||
},
|
||||
"InboxUpdateResponse": {
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"entry": true
|
||||
},
|
||||
"required": [
|
||||
"entry"
|
||||
],
|
||||
"title": "InboxUpdateResponse",
|
||||
"type": "object"
|
||||
},
|
||||
"InitializeCapabilities": {
|
||||
"description": "Client-declared capabilities negotiated during initialize.",
|
||||
"properties": {
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "List tracked inbox entries from `$CODEX_HOME/inbox`.",
|
||||
"title": "InboxListParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Tracked inbox records are returned as the on-disk JSON shape defined by the sort-inbox skill's thread record format.",
|
||||
"properties": {
|
||||
"data": {
|
||||
"items": true,
|
||||
"type": "array"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"data"
|
||||
],
|
||||
"title": "InboxListResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Update user-owned inbox tracking state for one record.",
|
||||
"properties": {
|
||||
"lastReadAt": {
|
||||
"description": "RFC 3339 timestamp to store in `tracking.last_read_at`.",
|
||||
"type": "string"
|
||||
},
|
||||
"threadId": {
|
||||
"description": "Exact inbox filename under `$CODEX_HOME/inbox`, for example `thread__20260319T170500Z__slack__C01234567__1712345678.000000.json`.",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"lastReadAt",
|
||||
"threadId"
|
||||
],
|
||||
"title": "InboxUpdateParams",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"properties": {
|
||||
"entry": true
|
||||
},
|
||||
"required": [
|
||||
"entry"
|
||||
],
|
||||
"title": "InboxUpdateResponse",
|
||||
"type": "object"
|
||||
}
|
||||
@@ -28,6 +28,8 @@ import type { FsReadFileParams } from "./v2/FsReadFileParams";
|
||||
import type { FsRemoveParams } from "./v2/FsRemoveParams";
|
||||
import type { FsWriteFileParams } from "./v2/FsWriteFileParams";
|
||||
import type { GetAccountParams } from "./v2/GetAccountParams";
|
||||
import type { InboxListParams } from "./v2/InboxListParams";
|
||||
import type { InboxUpdateParams } from "./v2/InboxUpdateParams";
|
||||
import type { ListMcpServerStatusParams } from "./v2/ListMcpServerStatusParams";
|
||||
import type { LoginAccountParams } from "./v2/LoginAccountParams";
|
||||
import type { McpServerOauthLoginParams } from "./v2/McpServerOauthLoginParams";
|
||||
@@ -61,4 +63,4 @@ import type { WindowsSandboxSetupStartParams } from "./v2/WindowsSandboxSetupSta
|
||||
/**
|
||||
* Request from the client to the server.
|
||||
*/
|
||||
export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, };
|
||||
export type ClientRequest ={ "method": "initialize", id: RequestId, params: InitializeParams, } | { "method": "thread/start", id: RequestId, params: ThreadStartParams, } | { "method": "thread/resume", id: RequestId, params: ThreadResumeParams, } | { "method": "thread/fork", id: RequestId, params: ThreadForkParams, } | { "method": "thread/archive", id: RequestId, params: ThreadArchiveParams, } | { "method": "thread/unsubscribe", id: RequestId, params: ThreadUnsubscribeParams, } | { "method": "thread/name/set", id: RequestId, params: ThreadSetNameParams, } | { "method": "thread/metadata/update", id: RequestId, params: ThreadMetadataUpdateParams, } | { "method": "thread/unarchive", id: RequestId, params: ThreadUnarchiveParams, } | { "method": "thread/compact/start", id: RequestId, params: ThreadCompactStartParams, } | { "method": "thread/shellCommand", id: RequestId, params: ThreadShellCommandParams, } | { "method": "thread/rollback", id: RequestId, params: ThreadRollbackParams, } | { "method": "thread/list", id: RequestId, params: ThreadListParams, } | { "method": "thread/loaded/list", id: RequestId, params: ThreadLoadedListParams, } | { "method": "thread/read", id: RequestId, params: ThreadReadParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "inbox/list", id: RequestId, params: InboxListParams, } | { "method": "inbox/update", id: RequestId, params: InboxUpdateParams, } | { "method": "fs/readFile", id: RequestId, params: FsReadFileParams, } | { "method": "fs/writeFile", id: RequestId, params: FsWriteFileParams, } | { "method": "fs/createDirectory", id: RequestId, params: FsCreateDirectoryParams, } | { "method": "fs/getMetadata", id: RequestId, params: FsGetMetadataParams, } | { "method": "fs/readDirectory", id: RequestId, params: FsReadDirectoryParams, } | { "method": "fs/remove", id: RequestId, params: FsRemoveParams, } | { "method": "fs/copy", id: RequestId, params: FsCopyParams, } | { "method": "skills/config/write", id: RequestId, params: SkillsConfigWriteParams, } | { "method": "plugin/install", id: RequestId, params: PluginInstallParams, } | { "method": "plugin/uninstall", id: RequestId, params: PluginUninstallParams, } | { "method": "turn/start", id: RequestId, params: TurnStartParams, } | { "method": "turn/steer", id: RequestId, params: TurnSteerParams, } | { "method": "turn/interrupt", id: RequestId, params: TurnInterruptParams, } | { "method": "review/start", id: RequestId, params: ReviewStartParams, } | { "method": "model/list", id: RequestId, params: ModelListParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "account/login/start", id: RequestId, params: LoginAccountParams, } | { "method": "account/login/cancel", id: RequestId, params: CancelLoginAccountParams, } | { "method": "account/logout", id: RequestId, params: undefined, } | { "method": "account/rateLimits/read", id: RequestId, params: undefined, } | { "method": "feedback/upload", id: RequestId, params: FeedbackUploadParams, } | { "method": "command/exec", id: RequestId, params: CommandExecParams, } | { "method": "command/exec/write", id: RequestId, params: CommandExecWriteParams, } | { "method": "command/exec/terminate", id: RequestId, params: CommandExecTerminateParams, } | { "method": "command/exec/resize", id: RequestId, params: CommandExecResizeParams, } | { "method": "config/read", id: RequestId, params: ConfigReadParams, } | { "method": "externalAgentConfig/detect", id: RequestId, params: ExternalAgentConfigDetectParams, } | { "method": "externalAgentConfig/import", id: RequestId, params: ExternalAgentConfigImportParams, } | { "method": "config/value/write", id: RequestId, params: ConfigValueWriteParams, } | { "method": "config/batchWrite", id: RequestId, params: ConfigBatchWriteParams, } | { "method": "configRequirements/read", id: RequestId, params: undefined, } | { "method": "account/read", id: RequestId, params: GetAccountParams, } | { "method": "getConversationSummary", id: RequestId, params: GetConversationSummaryParams, } | { "method": "gitDiffToRemote", id: RequestId, params: GitDiffToRemoteParams, } | { "method": "getAuthStatus", id: RequestId, params: GetAuthStatusParams, } | { "method": "fuzzyFileSearch", id: RequestId, params: FuzzyFileSearchParams, };
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* List tracked inbox entries from `$CODEX_HOME/inbox`.
|
||||
*/
|
||||
export type InboxListParams = Record<string, never>;
|
||||
@@ -0,0 +1,10 @@
|
||||
// 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 { JsonValue } from "../serde_json/JsonValue";
|
||||
|
||||
/**
|
||||
* Tracked inbox records are returned as the on-disk JSON shape defined by the
|
||||
* sort-inbox skill's thread record format.
|
||||
*/
|
||||
export type InboxListResponse = { data: Array<JsonValue>, };
|
||||
@@ -0,0 +1,17 @@
|
||||
// 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.
|
||||
|
||||
/**
|
||||
* Update user-owned inbox tracking state for one record.
|
||||
*/
|
||||
export type InboxUpdateParams = {
|
||||
/**
|
||||
* Exact inbox filename under `$CODEX_HOME/inbox`, for example
|
||||
* `thread__20260319T170500Z__slack__C01234567__1712345678.000000.json`.
|
||||
*/
|
||||
threadId: string,
|
||||
/**
|
||||
* RFC 3339 timestamp to store in `tracking.last_read_at`.
|
||||
*/
|
||||
lastReadAt: string, };
|
||||
@@ -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 { JsonValue } from "../serde_json/JsonValue";
|
||||
|
||||
export type InboxUpdateResponse = { entry: JsonValue, };
|
||||
@@ -131,6 +131,10 @@ export type { HookRunStatus } from "./HookRunStatus";
|
||||
export type { HookRunSummary } from "./HookRunSummary";
|
||||
export type { HookScope } from "./HookScope";
|
||||
export type { HookStartedNotification } from "./HookStartedNotification";
|
||||
export type { InboxListParams } from "./InboxListParams";
|
||||
export type { InboxListResponse } from "./InboxListResponse";
|
||||
export type { InboxUpdateParams } from "./InboxUpdateParams";
|
||||
export type { InboxUpdateResponse } from "./InboxUpdateResponse";
|
||||
export type { ItemCompletedNotification } from "./ItemCompletedNotification";
|
||||
export type { ItemGuardianApprovalReviewCompletedNotification } from "./ItemGuardianApprovalReviewCompletedNotification";
|
||||
export type { ItemGuardianApprovalReviewStartedNotification } from "./ItemGuardianApprovalReviewStartedNotification";
|
||||
|
||||
@@ -308,6 +308,14 @@ client_request_definitions! {
|
||||
params: v2::AppsListParams,
|
||||
response: v2::AppsListResponse,
|
||||
},
|
||||
InboxList => "inbox/list" {
|
||||
params: v2::InboxListParams,
|
||||
response: v2::InboxListResponse,
|
||||
},
|
||||
InboxUpdate => "inbox/update" {
|
||||
params: v2::InboxUpdateParams,
|
||||
response: v2::InboxUpdateResponse,
|
||||
},
|
||||
FsReadFile => "fs/readFile" {
|
||||
params: v2::FsReadFileParams,
|
||||
response: v2::FsReadFileResponse,
|
||||
|
||||
@@ -2061,6 +2061,40 @@ pub struct AppsListResponse {
|
||||
pub next_cursor: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Default, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
/// List tracked inbox entries from `$CODEX_HOME/inbox`.
|
||||
pub struct InboxListParams {}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
/// Tracked inbox records are returned as the on-disk JSON shape defined by the
|
||||
/// sort-inbox skill's thread record format.
|
||||
pub struct InboxListResponse {
|
||||
pub data: Vec<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
/// Update user-owned inbox tracking state for one record.
|
||||
pub struct InboxUpdateParams {
|
||||
/// Exact inbox filename under `$CODEX_HOME/inbox`, for example
|
||||
/// `thread__20260319T170500Z__slack__C01234567__1712345678.000000.json`.
|
||||
pub thread_id: String,
|
||||
/// RFC 3339 timestamp to store in `tracking.last_read_at`.
|
||||
pub last_read_at: String,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
pub struct InboxUpdateResponse {
|
||||
pub entry: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export_to = "v2/")]
|
||||
|
||||
@@ -1215,6 +1215,56 @@ The server also emits `app/list/updated` notifications whenever either source (a
|
||||
}
|
||||
```
|
||||
|
||||
## Inbox
|
||||
|
||||
Use `inbox/list` to load tracked inbox records from `$CODEX_HOME/inbox` (or `~/.codex/inbox` when `CODEX_HOME` is unset). The server returns the thread record JSON defined by the sort-inbox skill's format, ordered newest-first by filename, with user-owned tracking fields overlaid from `$CODEX_HOME/inbox/tracking.json`. Malformed `thread__*.json` files are skipped so one bad record does not block the rest.
|
||||
|
||||
```json
|
||||
{ "method": "inbox/list", "id": 51, "params": {} }
|
||||
{ "id": 51, "result": {
|
||||
"data": [
|
||||
{
|
||||
"schema_version": 1,
|
||||
"source": {
|
||||
"type": "slack",
|
||||
"channel_id": "C01234567",
|
||||
"thread_ts": "1712345678.000000",
|
||||
"thread_url": "https://..."
|
||||
},
|
||||
"participants": [],
|
||||
"tracking": {
|
||||
"last_refreshed_at": "2026-03-19T17:05:00-07:00"
|
||||
},
|
||||
"current_progress": "Short summary of where the thread stands now.",
|
||||
"context": {
|
||||
"slack_threads": [],
|
||||
"notion_pages": [],
|
||||
"google_docs": []
|
||||
},
|
||||
"timeline": []
|
||||
}
|
||||
]
|
||||
} }
|
||||
```
|
||||
|
||||
Use `inbox/update` to update user-owned tracking state for a single inbox record by filename. This writes to `$CODEX_HOME/inbox/tracking.json` instead of mutating the thread record file, so model-owned and user-owned writes do not overlap.
|
||||
|
||||
```json
|
||||
{ "method": "inbox/update", "id": 52, "params": {
|
||||
"threadId": "thread__20260320T012640Z__slack__C08MGJXUCUQ__1773969612.866769.json",
|
||||
"lastReadAt": "2026-03-20T09:15:00-07:00"
|
||||
} }
|
||||
{ "id": 52, "result": {
|
||||
"entry": {
|
||||
"schema_version": 1,
|
||||
"tracking": {
|
||||
"last_read_at": "2026-03-20T09:15:00-07:00",
|
||||
"last_refreshed_at": "2026-03-19T18:54:47-07:00"
|
||||
}
|
||||
}
|
||||
} }
|
||||
```
|
||||
|
||||
Invoke an app by inserting `$<app-slug>` in the text input. The slug is derived from the app name and lowercased with non-alphanumeric characters replaced by `-` (for example, "Demo App" becomes `$demo-app`). Add a `mention` input item (recommended) so the server uses the exact `app://<connector-id>` path rather than guessing by name. Plugins use the same `mention` item shape, but with `plugin://<plugin-name>@<marketplace-name>` paths from `plugin/list`.
|
||||
|
||||
Example:
|
||||
|
||||
@@ -64,6 +64,10 @@ 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::InboxListParams;
|
||||
use codex_app_server_protocol::InboxListResponse;
|
||||
use codex_app_server_protocol::InboxUpdateParams;
|
||||
use codex_app_server_protocol::InboxUpdateResponse;
|
||||
use codex_app_server_protocol::JSONRPCErrorError;
|
||||
use codex_app_server_protocol::ListMcpServerStatusParams;
|
||||
use codex_app_server_protocol::ListMcpServerStatusResponse;
|
||||
@@ -310,6 +314,7 @@ use uuid::Uuid;
|
||||
use codex_app_server_protocol::ServerRequest;
|
||||
|
||||
mod apps_list_helpers;
|
||||
mod inbox_list_helpers;
|
||||
mod plugin_app_helpers;
|
||||
|
||||
use crate::filters::compute_source_filters;
|
||||
@@ -717,6 +722,14 @@ impl CodexMessageProcessor {
|
||||
self.apps_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::InboxList { request_id, params } => {
|
||||
self.inbox_list(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::InboxUpdate { request_id, params } => {
|
||||
self.inbox_update(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
}
|
||||
ClientRequest::SkillsConfigWrite { request_id, params } => {
|
||||
self.skills_config_write(to_connection_request_id(request_id), params)
|
||||
.await;
|
||||
@@ -5201,6 +5214,50 @@ impl CodexMessageProcessor {
|
||||
});
|
||||
}
|
||||
|
||||
async fn inbox_list(&self, request_id: ConnectionRequestId, params: InboxListParams) {
|
||||
let InboxListParams {} = params;
|
||||
|
||||
match inbox_list_helpers::load_inbox_entries(&self.config.codex_home).await {
|
||||
Ok(data) => {
|
||||
self.outgoing
|
||||
.send_response(request_id, InboxListResponse { data })
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
self.send_internal_error(request_id, format!("failed to list inbox: {err}"))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn inbox_update(&self, request_id: ConnectionRequestId, params: InboxUpdateParams) {
|
||||
let InboxUpdateParams {
|
||||
thread_id,
|
||||
last_read_at,
|
||||
} = params;
|
||||
|
||||
match inbox_list_helpers::update_inbox_entry_last_read_at(
|
||||
&self.config.codex_home,
|
||||
&thread_id,
|
||||
&last_read_at,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(entry) => {
|
||||
self.outgoing
|
||||
.send_response(request_id, InboxUpdateResponse { entry })
|
||||
.await;
|
||||
}
|
||||
Err(inbox_list_helpers::InboxUpdateError::InvalidRequest(message)) => {
|
||||
self.send_invalid_request_error(request_id, message).await;
|
||||
}
|
||||
Err(inbox_list_helpers::InboxUpdateError::Io(err)) => {
|
||||
self.send_internal_error(request_id, format!("failed to update inbox: {err}"))
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn apps_list_task(
|
||||
outgoing: Arc<OutgoingMessageSender>,
|
||||
request_id: ConnectionRequestId,
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
|
||||
use chrono::DateTime;
|
||||
use serde_json::Map;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use tokio::fs;
|
||||
use tracing::warn;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) enum InboxUpdateError {
|
||||
InvalidRequest(String),
|
||||
Io(io::Error),
|
||||
}
|
||||
|
||||
impl From<io::Error> for InboxUpdateError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
Self::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn load_inbox_entries(codex_home: &Path) -> io::Result<Vec<Value>> {
|
||||
let inbox_dir = codex_home.join("inbox");
|
||||
let mut dir = match fs::read_dir(&inbox_dir).await {
|
||||
Ok(dir) => dir,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
|
||||
let user_tracking_by_thread = match load_user_tracking_by_thread(codex_home).await {
|
||||
Ok(user_tracking_by_thread) => user_tracking_by_thread,
|
||||
Err(err) => {
|
||||
warn!("Skipping malformed inbox tracking file: {err}");
|
||||
Map::new()
|
||||
}
|
||||
};
|
||||
|
||||
let mut paths = Vec::new();
|
||||
while let Some(entry) = dir.next_entry().await? {
|
||||
let file_name = entry.file_name();
|
||||
let Some(file_name) = file_name.to_str() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if file_name.starts_with("thread__") && file_name.ends_with(".json") {
|
||||
paths.push((file_name.to_owned(), entry.path()));
|
||||
}
|
||||
}
|
||||
|
||||
paths.sort_by(|a, b| b.0.cmp(&a.0));
|
||||
|
||||
let mut data = Vec::with_capacity(paths.len());
|
||||
for (file_name, path) in paths {
|
||||
let contents = fs::read_to_string(&path).await?;
|
||||
match serde_json::from_str::<Value>(&contents) {
|
||||
Ok(mut value) => {
|
||||
remove_thread_owned_last_read_at(&mut value);
|
||||
apply_user_tracking(&mut value, user_tracking_by_thread.get(&file_name));
|
||||
data.push(value);
|
||||
}
|
||||
Err(err) => {
|
||||
warn!("Skipping malformed inbox entry {file_name}: {err}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
pub(super) async fn update_inbox_entry_last_read_at(
|
||||
codex_home: &Path,
|
||||
thread_id: &str,
|
||||
last_read_at: &str,
|
||||
) -> Result<Value, InboxUpdateError> {
|
||||
if thread_id.is_empty() {
|
||||
return Err(InboxUpdateError::InvalidRequest(
|
||||
"thread_id must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if thread_id.contains(['/', '\\']) {
|
||||
return Err(InboxUpdateError::InvalidRequest(format!(
|
||||
"thread_id must be a filename, not a path: {thread_id}"
|
||||
)));
|
||||
}
|
||||
|
||||
if DateTime::parse_from_rfc3339(last_read_at).is_err() {
|
||||
return Err(InboxUpdateError::InvalidRequest(format!(
|
||||
"last_read_at must be an RFC 3339 timestamp: {last_read_at}"
|
||||
)));
|
||||
}
|
||||
|
||||
let thread_path = codex_home.join("inbox").join(thread_id);
|
||||
if let Err(err) = fs::metadata(&thread_path).await {
|
||||
if err.kind() == io::ErrorKind::NotFound {
|
||||
return Err(InboxUpdateError::InvalidRequest(format!(
|
||||
"inbox entry not found: {thread_id}"
|
||||
)));
|
||||
}
|
||||
|
||||
return Err(err.into());
|
||||
}
|
||||
|
||||
let mut user_tracking_by_thread = match load_user_tracking_by_thread(codex_home).await {
|
||||
Ok(user_tracking_by_thread) => user_tracking_by_thread,
|
||||
Err(err) => {
|
||||
return Err(InboxUpdateError::InvalidRequest(format!(
|
||||
"inbox tracking file is not valid JSON: {err}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let user_tracking = user_tracking_by_thread
|
||||
.entry(thread_id.to_string())
|
||||
.or_insert_with(|| json!({}));
|
||||
let Some(user_tracking_object) = user_tracking.as_object_mut() else {
|
||||
return Err(InboxUpdateError::InvalidRequest(format!(
|
||||
"tracking entry for {thread_id} must be a JSON object"
|
||||
)));
|
||||
};
|
||||
user_tracking_object.insert("last_read_at".to_string(), json!(last_read_at));
|
||||
|
||||
let tracking_path = codex_home.join("inbox").join("tracking.json");
|
||||
let serialized = serde_json::to_string_pretty(&user_tracking_by_thread).map_err(|err| {
|
||||
InboxUpdateError::InvalidRequest(format!("failed to serialize tracking file: {err}"))
|
||||
})?;
|
||||
fs::write(&tracking_path, format!("{serialized}\n")).await?;
|
||||
|
||||
let contents = match fs::read_to_string(&thread_path).await {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||
return Err(InboxUpdateError::InvalidRequest(format!(
|
||||
"inbox entry not found: {thread_id}"
|
||||
)));
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err.into());
|
||||
}
|
||||
};
|
||||
|
||||
let mut entry = serde_json::from_str::<Value>(&contents).map_err(|err| {
|
||||
InboxUpdateError::InvalidRequest(format!("inbox entry is not valid JSON: {err}"))
|
||||
})?;
|
||||
|
||||
remove_thread_owned_last_read_at(&mut entry);
|
||||
apply_user_tracking(&mut entry, user_tracking_by_thread.get(thread_id));
|
||||
|
||||
Ok(entry)
|
||||
}
|
||||
|
||||
async fn load_user_tracking_by_thread(codex_home: &Path) -> Result<Map<String, Value>, String> {
|
||||
let path = codex_home.join("inbox").join("tracking.json");
|
||||
let contents = match fs::read_to_string(path).await {
|
||||
Ok(contents) => contents,
|
||||
Err(err) if err.kind() == io::ErrorKind::NotFound => {
|
||||
return Ok(Map::new());
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err.to_string());
|
||||
}
|
||||
};
|
||||
|
||||
serde_json::from_str::<Map<String, Value>>(&contents).map_err(|err| err.to_string())
|
||||
}
|
||||
|
||||
fn apply_user_tracking(entry: &mut Value, user_tracking: Option<&Value>) {
|
||||
let Some(user_tracking) = user_tracking else {
|
||||
return;
|
||||
};
|
||||
let Some(user_tracking_object) = user_tracking.as_object() else {
|
||||
return;
|
||||
};
|
||||
let Some(entry_object) = entry.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let tracking = entry_object
|
||||
.entry("tracking".to_string())
|
||||
.or_insert_with(|| json!({}));
|
||||
let Some(tracking_object) = tracking.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
for (key, value) in user_tracking_object {
|
||||
tracking_object.insert(key.clone(), value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_thread_owned_last_read_at(entry: &mut Value) {
|
||||
let Some(entry_object) = entry.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
let Some(tracking) = entry_object.get_mut("tracking") else {
|
||||
return;
|
||||
};
|
||||
let Some(tracking_object) = tracking.as_object_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
tracking_object.remove("last_read_at");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::Result;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_inbox_entries_returns_newest_first_and_skips_malformed_files() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let inbox_dir = tempdir.path().join("inbox");
|
||||
fs::create_dir(&inbox_dir).await?;
|
||||
|
||||
fs::write(
|
||||
inbox_dir.join("thread__20260319T202114Z__slack__C1__111.000000.json"),
|
||||
json!({
|
||||
"schema_version": 1,
|
||||
"current_progress": "older"
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.await?;
|
||||
fs::write(
|
||||
inbox_dir.join("thread__20260320T012640Z__slack__C2__222.000000.json"),
|
||||
json!({
|
||||
"schema_version": 1,
|
||||
"current_progress": "newer",
|
||||
"tracking": {
|
||||
"last_read_at": "2026-03-19T16:20:00-07:00"
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.await?;
|
||||
fs::write(
|
||||
inbox_dir.join("thread__20260320T020000Z__slack__C3__333.000000.json"),
|
||||
"{",
|
||||
)
|
||||
.await?;
|
||||
fs::write(inbox_dir.join("notes.txt"), "ignored").await?;
|
||||
fs::write(
|
||||
inbox_dir.join("tracking.json"),
|
||||
json!({
|
||||
"thread__20260320T012640Z__slack__C2__222.000000.json": {
|
||||
"last_read_at": "2026-03-20T09:15:00-07:00"
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let entries = load_inbox_entries(tempdir.path()).await?;
|
||||
|
||||
assert_eq!(
|
||||
entries,
|
||||
vec![
|
||||
json!({
|
||||
"schema_version": 1,
|
||||
"current_progress": "newer",
|
||||
"tracking": {
|
||||
"last_read_at": "2026-03-20T09:15:00-07:00"
|
||||
}
|
||||
}),
|
||||
json!({
|
||||
"schema_version": 1,
|
||||
"current_progress": "older"
|
||||
}),
|
||||
]
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn load_inbox_entries_returns_empty_when_inbox_dir_is_missing() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
let entries = load_inbox_entries(tempdir.path()).await?;
|
||||
|
||||
assert_eq!(entries, Vec::<Value>::new());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_inbox_entry_last_read_at_writes_tracking_file_and_leaves_thread_file_unchanged()
|
||||
-> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
let inbox_dir = tempdir.path().join("inbox");
|
||||
fs::create_dir(&inbox_dir).await?;
|
||||
let thread_id = "thread__20260320T012640Z__slack__C08MGJXUCUQ__1773969612.866769.json";
|
||||
let thread_path = inbox_dir.join(thread_id);
|
||||
let original_thread_entry = json!({
|
||||
"schema_version": 1,
|
||||
"source": {
|
||||
"type": "slack",
|
||||
"channel_id": "C08MGJXUCUQ",
|
||||
"thread_ts": "1773969612.866769"
|
||||
},
|
||||
"tracking": {
|
||||
"last_refreshed_at": "2026-03-19T18:54:47-07:00"
|
||||
},
|
||||
"timeline": []
|
||||
});
|
||||
fs::write(&thread_path, original_thread_entry.to_string()).await?;
|
||||
|
||||
let entry =
|
||||
update_inbox_entry_last_read_at(tempdir.path(), thread_id, "2026-03-20T09:15:00-07:00")
|
||||
.await
|
||||
.expect("update succeeds");
|
||||
|
||||
assert_eq!(
|
||||
entry,
|
||||
json!({
|
||||
"schema_version": 1,
|
||||
"source": {
|
||||
"type": "slack",
|
||||
"channel_id": "C08MGJXUCUQ",
|
||||
"thread_ts": "1773969612.866769"
|
||||
},
|
||||
"tracking": {
|
||||
"last_read_at": "2026-03-20T09:15:00-07:00",
|
||||
"last_refreshed_at": "2026-03-19T18:54:47-07:00"
|
||||
},
|
||||
"timeline": []
|
||||
})
|
||||
);
|
||||
|
||||
let reloaded_thread =
|
||||
serde_json::from_str::<Value>(&fs::read_to_string(&thread_path).await?)?;
|
||||
assert_eq!(reloaded_thread, original_thread_entry);
|
||||
|
||||
let reloaded_tracking = serde_json::from_str::<Value>(
|
||||
&fs::read_to_string(inbox_dir.join("tracking.json")).await?,
|
||||
)?;
|
||||
assert_eq!(
|
||||
reloaded_tracking,
|
||||
json!({
|
||||
thread_id: {
|
||||
"last_read_at": "2026-03-20T09:15:00-07:00"
|
||||
}
|
||||
})
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_inbox_entry_last_read_at_rejects_path_traversal() -> Result<()> {
|
||||
let tempdir = TempDir::new()?;
|
||||
|
||||
let err = update_inbox_entry_last_read_at(
|
||||
tempdir.path(),
|
||||
"../thread__20260320T012640Z__slack__C08MGJXUCUQ__1773969612.866769.json",
|
||||
"2026-03-20T09:15:00-07:00",
|
||||
)
|
||||
.await
|
||||
.expect_err("path-like ids are rejected");
|
||||
|
||||
match err {
|
||||
InboxUpdateError::InvalidRequest(message) => {
|
||||
assert_eq!(
|
||||
message,
|
||||
"thread_id must be a filename, not a path: ../thread__20260320T012640Z__slack__C08MGJXUCUQ__1773969612.866769.json"
|
||||
);
|
||||
}
|
||||
InboxUpdateError::Io(err) => panic!("unexpected io error: {err}"),
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user