diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index 2367cb5c2ad1..12b61cb48bd5 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -1866,7 +1866,6 @@ dependencies = [ "codex-config", "codex-core", "codex-core-plugins", - "codex-device-key", "codex-exec-server", "codex-external-agent-migration", "codex-external-agent-sessions", @@ -2637,22 +2636,6 @@ dependencies = [ "serde_json", ] -[[package]] -name = "codex-device-key" -version = "0.0.0" -dependencies = [ - "async-trait", - "base64 0.22.1", - "p256", - "pretty_assertions", - "rand 0.9.3", - "serde", - "serde_json", - "thiserror 2.0.18", - "tokio", - "url", -] - [[package]] name = "codex-exec" version = "0.0.0" diff --git a/codex-rs/Cargo.toml b/codex-rs/Cargo.toml index f1673d3bab76..cf2332a2762c 100644 --- a/codex-rs/Cargo.toml +++ b/codex-rs/Cargo.toml @@ -30,7 +30,6 @@ members = [ "collaboration-mode-templates", "connectors", "config", - "device-key", "shell-command", "shell-escalation", "skills", @@ -154,7 +153,6 @@ codex-core = { path = "core" } codex-core-api = { path = "core-api" } codex-core-plugins = { path = "core-plugins" } codex-core-skills = { path = "core-skills" } -codex-device-key = { path = "device-key" } codex-exec = { path = "exec" } codex-file-system = { path = "file-system" } codex-exec-server = { path = "exec-server" } @@ -319,7 +317,6 @@ os_info = "3.12.0" owo-colors = "4.3.0" path-absolutize = "3.1.1" pathdiff = "0.2" -p256 = "0.13.2" portable-pty = "0.9.0" predicates = "3" pretty_assertions = "1.4.1" diff --git a/codex-rs/app-server-protocol/schema/json/ClientRequest.json b/codex-rs/app-server-protocol/schema/json/ClientRequest.json index 0bef76c74cda..cac1c33d9bb3 100644 --- a/codex-rs/app-server-protocol/schema/json/ClientRequest.json +++ b/codex-rs/app-server-protocol/schema/json/ClientRequest.json @@ -533,200 +533,6 @@ } ] }, - "DeviceKeyCreateParams": { - "description": "Create a controller-local device key with a random key id.", - "properties": { - "accountUserId": { - "type": "string" - }, - "clientId": { - "type": "string" - }, - "protectionPolicy": { - "anyOf": [ - { - "$ref": "#/definitions/DeviceKeyProtectionPolicy" - }, - { - "type": "null" - } - ], - "description": "Defaults to `hardware_only` when omitted." - } - }, - "required": [ - "accountUserId", - "clientId" - ], - "type": "object" - }, - "DeviceKeyProtectionPolicy": { - "description": "Protection policy for creating or loading a controller-local device key.", - "enum": [ - "hardware_only", - "allow_os_protected_nonextractable" - ], - "type": "string" - }, - "DeviceKeyPublicParams": { - "description": "Fetch a controller-local device key public key by id.", - "properties": { - "keyId": { - "type": "string" - } - }, - "required": [ - "keyId" - ], - "type": "object" - }, - "DeviceKeySignParams": { - "description": "Sign an accepted structured payload with a controller-local device key.", - "properties": { - "keyId": { - "type": "string" - }, - "payload": { - "$ref": "#/definitions/DeviceKeySignPayload" - } - }, - "required": [ - "keyId", - "payload" - ], - "type": "object" - }, - "DeviceKeySignPayload": { - "description": "Structured payloads accepted by `device/key/sign`.", - "oneOf": [ - { - "description": "Payload bound to one remote-control controller websocket `/client` connection challenge.", - "properties": { - "accountUserId": { - "type": "string" - }, - "audience": { - "$ref": "#/definitions/RemoteControlClientConnectionAudience" - }, - "clientId": { - "type": "string" - }, - "nonce": { - "type": "string" - }, - "scopes": { - "description": "Must contain exactly `remote_control_controller_websocket`.", - "items": { - "type": "string" - }, - "type": "array" - }, - "sessionId": { - "description": "Backend-issued websocket session id that this proof authorizes.", - "type": "string" - }, - "targetOrigin": { - "description": "Origin of the backend endpoint that issued the challenge and will verify this proof.", - "type": "string" - }, - "targetPath": { - "description": "Websocket route path that this proof authorizes.", - "type": "string" - }, - "tokenExpiresAt": { - "description": "Remote-control token expiration as Unix seconds.", - "format": "int64", - "type": "integer" - }, - "tokenSha256Base64url": { - "description": "SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url.", - "type": "string" - }, - "type": { - "enum": [ - "remoteControlClientConnection" - ], - "title": "RemoteControlClientConnectionDeviceKeySignPayloadType", - "type": "string" - } - }, - "required": [ - "accountUserId", - "audience", - "clientId", - "nonce", - "scopes", - "sessionId", - "targetOrigin", - "targetPath", - "tokenExpiresAt", - "tokenSha256Base64url", - "type" - ], - "title": "RemoteControlClientConnectionDeviceKeySignPayload", - "type": "object" - }, - { - "description": "Payload bound to a remote-control client `/client/enroll` ownership challenge.", - "properties": { - "accountUserId": { - "type": "string" - }, - "audience": { - "$ref": "#/definitions/RemoteControlClientEnrollmentAudience" - }, - "challengeExpiresAt": { - "description": "Enrollment challenge expiration as Unix seconds.", - "format": "int64", - "type": "integer" - }, - "challengeId": { - "description": "Backend-issued enrollment challenge id that this proof authorizes.", - "type": "string" - }, - "clientId": { - "type": "string" - }, - "deviceIdentitySha256Base64url": { - "description": "SHA-256 of the requested device identity operation, encoded as unpadded base64url.", - "type": "string" - }, - "nonce": { - "type": "string" - }, - "targetOrigin": { - "description": "Origin of the backend endpoint that issued the challenge and will verify this proof.", - "type": "string" - }, - "targetPath": { - "description": "HTTP route path that this proof authorizes.", - "type": "string" - }, - "type": { - "enum": [ - "remoteControlClientEnrollment" - ], - "title": "RemoteControlClientEnrollmentDeviceKeySignPayloadType", - "type": "string" - } - }, - "required": [ - "accountUserId", - "audience", - "challengeExpiresAt", - "challengeId", - "clientId", - "deviceIdentitySha256Base64url", - "nonce", - "targetOrigin", - "targetPath", - "type" - ], - "title": "RemoteControlClientEnrollmentDeviceKeySignPayload", - "type": "object" - } - ] - }, "DynamicToolSpec": { "properties": { "deferLoading": { @@ -2504,20 +2310,6 @@ } ] }, - "RemoteControlClientConnectionAudience": { - "description": "Audience for a remote-control client connection device-key proof.", - "enum": [ - "remote_control_client_websocket" - ], - "type": "string" - }, - "RemoteControlClientEnrollmentAudience": { - "description": "Audience for a remote-control client enrollment device-key proof.", - "enum": [ - "remote_control_client_enrollment" - ], - "type": "string" - }, "RequestId": { "anyOf": [ { @@ -5308,78 +5100,6 @@ "title": "App/listRequest", "type": "object" }, - { - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "device/key/create" - ], - "title": "Device/key/createRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/DeviceKeyCreateParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Device/key/createRequest", - "type": "object" - }, - { - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "device/key/public" - ], - "title": "Device/key/publicRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/DeviceKeyPublicParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Device/key/publicRequest", - "type": "object" - }, - { - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "device/key/sign" - ], - "title": "Device/key/signRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/DeviceKeySignParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Device/key/signRequest", - "type": "object" - }, { "properties": { "id": { @@ -6461,4 +6181,4 @@ } ], "title": "ClientRequest" -} \ No newline at end of file +} diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index 06ccdb48b263..e584e5524fc1 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -906,78 +906,6 @@ "title": "App/listRequest", "type": "object" }, - { - "properties": { - "id": { - "$ref": "#/definitions/v2/RequestId" - }, - "method": { - "enum": [ - "device/key/create" - ], - "title": "Device/key/createRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/v2/DeviceKeyCreateParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Device/key/createRequest", - "type": "object" - }, - { - "properties": { - "id": { - "$ref": "#/definitions/v2/RequestId" - }, - "method": { - "enum": [ - "device/key/public" - ], - "title": "Device/key/publicRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/v2/DeviceKeyPublicParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Device/key/publicRequest", - "type": "object" - }, - { - "properties": { - "id": { - "$ref": "#/definitions/v2/RequestId" - }, - "method": { - "enum": [ - "device/key/sign" - ], - "title": "Device/key/signRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/v2/DeviceKeySignParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Device/key/signRequest", - "type": "object" - }, { "properties": { "id": { @@ -7947,300 +7875,6 @@ "title": "DeprecationNoticeNotification", "type": "object" }, - "DeviceKeyAlgorithm": { - "description": "Device-key algorithm reported at enrollment and signing boundaries.", - "enum": [ - "ecdsa_p256_sha256" - ], - "type": "string" - }, - "DeviceKeyCreateParams": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Create a controller-local device key with a random key id.", - "properties": { - "accountUserId": { - "type": "string" - }, - "clientId": { - "type": "string" - }, - "protectionPolicy": { - "anyOf": [ - { - "$ref": "#/definitions/v2/DeviceKeyProtectionPolicy" - }, - { - "type": "null" - } - ], - "description": "Defaults to `hardware_only` when omitted." - } - }, - "required": [ - "accountUserId", - "clientId" - ], - "title": "DeviceKeyCreateParams", - "type": "object" - }, - "DeviceKeyCreateResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Device-key metadata and public key returned by create/public APIs.", - "properties": { - "algorithm": { - "$ref": "#/definitions/v2/DeviceKeyAlgorithm" - }, - "keyId": { - "type": "string" - }, - "protectionClass": { - "$ref": "#/definitions/v2/DeviceKeyProtectionClass" - }, - "publicKeySpkiDerBase64": { - "description": "SubjectPublicKeyInfo DER encoded as base64.", - "type": "string" - } - }, - "required": [ - "algorithm", - "keyId", - "protectionClass", - "publicKeySpkiDerBase64" - ], - "title": "DeviceKeyCreateResponse", - "type": "object" - }, - "DeviceKeyProtectionClass": { - "description": "Platform protection class for a controller-local device key.", - "enum": [ - "hardware_secure_enclave", - "hardware_tpm", - "os_protected_nonextractable" - ], - "type": "string" - }, - "DeviceKeyProtectionPolicy": { - "description": "Protection policy for creating or loading a controller-local device key.", - "enum": [ - "hardware_only", - "allow_os_protected_nonextractable" - ], - "type": "string" - }, - "DeviceKeyPublicParams": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Fetch a controller-local device key public key by id.", - "properties": { - "keyId": { - "type": "string" - } - }, - "required": [ - "keyId" - ], - "title": "DeviceKeyPublicParams", - "type": "object" - }, - "DeviceKeyPublicResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Device-key public metadata returned by `device/key/public`.", - "properties": { - "algorithm": { - "$ref": "#/definitions/v2/DeviceKeyAlgorithm" - }, - "keyId": { - "type": "string" - }, - "protectionClass": { - "$ref": "#/definitions/v2/DeviceKeyProtectionClass" - }, - "publicKeySpkiDerBase64": { - "description": "SubjectPublicKeyInfo DER encoded as base64.", - "type": "string" - } - }, - "required": [ - "algorithm", - "keyId", - "protectionClass", - "publicKeySpkiDerBase64" - ], - "title": "DeviceKeyPublicResponse", - "type": "object" - }, - "DeviceKeySignParams": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Sign an accepted structured payload with a controller-local device key.", - "properties": { - "keyId": { - "type": "string" - }, - "payload": { - "$ref": "#/definitions/v2/DeviceKeySignPayload" - } - }, - "required": [ - "keyId", - "payload" - ], - "title": "DeviceKeySignParams", - "type": "object" - }, - "DeviceKeySignPayload": { - "description": "Structured payloads accepted by `device/key/sign`.", - "oneOf": [ - { - "description": "Payload bound to one remote-control controller websocket `/client` connection challenge.", - "properties": { - "accountUserId": { - "type": "string" - }, - "audience": { - "$ref": "#/definitions/v2/RemoteControlClientConnectionAudience" - }, - "clientId": { - "type": "string" - }, - "nonce": { - "type": "string" - }, - "scopes": { - "description": "Must contain exactly `remote_control_controller_websocket`.", - "items": { - "type": "string" - }, - "type": "array" - }, - "sessionId": { - "description": "Backend-issued websocket session id that this proof authorizes.", - "type": "string" - }, - "targetOrigin": { - "description": "Origin of the backend endpoint that issued the challenge and will verify this proof.", - "type": "string" - }, - "targetPath": { - "description": "Websocket route path that this proof authorizes.", - "type": "string" - }, - "tokenExpiresAt": { - "description": "Remote-control token expiration as Unix seconds.", - "format": "int64", - "type": "integer" - }, - "tokenSha256Base64url": { - "description": "SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url.", - "type": "string" - }, - "type": { - "enum": [ - "remoteControlClientConnection" - ], - "title": "RemoteControlClientConnectionDeviceKeySignPayloadType", - "type": "string" - } - }, - "required": [ - "accountUserId", - "audience", - "clientId", - "nonce", - "scopes", - "sessionId", - "targetOrigin", - "targetPath", - "tokenExpiresAt", - "tokenSha256Base64url", - "type" - ], - "title": "RemoteControlClientConnectionDeviceKeySignPayload", - "type": "object" - }, - { - "description": "Payload bound to a remote-control client `/client/enroll` ownership challenge.", - "properties": { - "accountUserId": { - "type": "string" - }, - "audience": { - "$ref": "#/definitions/v2/RemoteControlClientEnrollmentAudience" - }, - "challengeExpiresAt": { - "description": "Enrollment challenge expiration as Unix seconds.", - "format": "int64", - "type": "integer" - }, - "challengeId": { - "description": "Backend-issued enrollment challenge id that this proof authorizes.", - "type": "string" - }, - "clientId": { - "type": "string" - }, - "deviceIdentitySha256Base64url": { - "description": "SHA-256 of the requested device identity operation, encoded as unpadded base64url.", - "type": "string" - }, - "nonce": { - "type": "string" - }, - "targetOrigin": { - "description": "Origin of the backend endpoint that issued the challenge and will verify this proof.", - "type": "string" - }, - "targetPath": { - "description": "HTTP route path that this proof authorizes.", - "type": "string" - }, - "type": { - "enum": [ - "remoteControlClientEnrollment" - ], - "title": "RemoteControlClientEnrollmentDeviceKeySignPayloadType", - "type": "string" - } - }, - "required": [ - "accountUserId", - "audience", - "challengeExpiresAt", - "challengeId", - "clientId", - "deviceIdentitySha256Base64url", - "nonce", - "targetOrigin", - "targetPath", - "type" - ], - "title": "RemoteControlClientEnrollmentDeviceKeySignPayload", - "type": "object" - } - ] - }, - "DeviceKeySignResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "ASN.1 DER signature returned by `device/key/sign`.", - "properties": { - "algorithm": { - "$ref": "#/definitions/v2/DeviceKeyAlgorithm" - }, - "signatureDerBase64": { - "description": "ECDSA signature DER encoded as base64.", - "type": "string" - }, - "signedPayloadBase64": { - "description": "Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte string directly and must not reserialize `payload`.", - "type": "string" - } - }, - "required": [ - "algorithm", - "signatureDerBase64", - "signedPayloadBase64" - ], - "title": "DeviceKeySignResponse", - "type": "object" - }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -13573,20 +13207,6 @@ "title": "ReasoningTextDeltaNotification", "type": "object" }, - "RemoteControlClientConnectionAudience": { - "description": "Audience for a remote-control client connection device-key proof.", - "enum": [ - "remote_control_client_websocket" - ], - "type": "string" - }, - "RemoteControlClientEnrollmentAudience": { - "description": "Audience for a remote-control client enrollment device-key proof.", - "enum": [ - "remote_control_client_enrollment" - ], - "type": "string" - }, "RemoteControlConnectionStatus": { "enum": [ "disabled", @@ -18732,4 +18352,4 @@ }, "title": "CodexAppServerProtocol", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index 852cc2489d08..628c0bc79ea1 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -1665,78 +1665,6 @@ "title": "App/listRequest", "type": "object" }, - { - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "device/key/create" - ], - "title": "Device/key/createRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/DeviceKeyCreateParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Device/key/createRequest", - "type": "object" - }, - { - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "device/key/public" - ], - "title": "Device/key/publicRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/DeviceKeyPublicParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Device/key/publicRequest", - "type": "object" - }, - { - "properties": { - "id": { - "$ref": "#/definitions/RequestId" - }, - "method": { - "enum": [ - "device/key/sign" - ], - "title": "Device/key/signRequestMethod", - "type": "string" - }, - "params": { - "$ref": "#/definitions/DeviceKeySignParams" - } - }, - "required": [ - "id", - "method", - "params" - ], - "title": "Device/key/signRequest", - "type": "object" - }, { "properties": { "id": { @@ -4403,300 +4331,6 @@ "title": "DeprecationNoticeNotification", "type": "object" }, - "DeviceKeyAlgorithm": { - "description": "Device-key algorithm reported at enrollment and signing boundaries.", - "enum": [ - "ecdsa_p256_sha256" - ], - "type": "string" - }, - "DeviceKeyCreateParams": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Create a controller-local device key with a random key id.", - "properties": { - "accountUserId": { - "type": "string" - }, - "clientId": { - "type": "string" - }, - "protectionPolicy": { - "anyOf": [ - { - "$ref": "#/definitions/DeviceKeyProtectionPolicy" - }, - { - "type": "null" - } - ], - "description": "Defaults to `hardware_only` when omitted." - } - }, - "required": [ - "accountUserId", - "clientId" - ], - "title": "DeviceKeyCreateParams", - "type": "object" - }, - "DeviceKeyCreateResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Device-key metadata and public key returned by create/public APIs.", - "properties": { - "algorithm": { - "$ref": "#/definitions/DeviceKeyAlgorithm" - }, - "keyId": { - "type": "string" - }, - "protectionClass": { - "$ref": "#/definitions/DeviceKeyProtectionClass" - }, - "publicKeySpkiDerBase64": { - "description": "SubjectPublicKeyInfo DER encoded as base64.", - "type": "string" - } - }, - "required": [ - "algorithm", - "keyId", - "protectionClass", - "publicKeySpkiDerBase64" - ], - "title": "DeviceKeyCreateResponse", - "type": "object" - }, - "DeviceKeyProtectionClass": { - "description": "Platform protection class for a controller-local device key.", - "enum": [ - "hardware_secure_enclave", - "hardware_tpm", - "os_protected_nonextractable" - ], - "type": "string" - }, - "DeviceKeyProtectionPolicy": { - "description": "Protection policy for creating or loading a controller-local device key.", - "enum": [ - "hardware_only", - "allow_os_protected_nonextractable" - ], - "type": "string" - }, - "DeviceKeyPublicParams": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Fetch a controller-local device key public key by id.", - "properties": { - "keyId": { - "type": "string" - } - }, - "required": [ - "keyId" - ], - "title": "DeviceKeyPublicParams", - "type": "object" - }, - "DeviceKeyPublicResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Device-key public metadata returned by `device/key/public`.", - "properties": { - "algorithm": { - "$ref": "#/definitions/DeviceKeyAlgorithm" - }, - "keyId": { - "type": "string" - }, - "protectionClass": { - "$ref": "#/definitions/DeviceKeyProtectionClass" - }, - "publicKeySpkiDerBase64": { - "description": "SubjectPublicKeyInfo DER encoded as base64.", - "type": "string" - } - }, - "required": [ - "algorithm", - "keyId", - "protectionClass", - "publicKeySpkiDerBase64" - ], - "title": "DeviceKeyPublicResponse", - "type": "object" - }, - "DeviceKeySignParams": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Sign an accepted structured payload with a controller-local device key.", - "properties": { - "keyId": { - "type": "string" - }, - "payload": { - "$ref": "#/definitions/DeviceKeySignPayload" - } - }, - "required": [ - "keyId", - "payload" - ], - "title": "DeviceKeySignParams", - "type": "object" - }, - "DeviceKeySignPayload": { - "description": "Structured payloads accepted by `device/key/sign`.", - "oneOf": [ - { - "description": "Payload bound to one remote-control controller websocket `/client` connection challenge.", - "properties": { - "accountUserId": { - "type": "string" - }, - "audience": { - "$ref": "#/definitions/RemoteControlClientConnectionAudience" - }, - "clientId": { - "type": "string" - }, - "nonce": { - "type": "string" - }, - "scopes": { - "description": "Must contain exactly `remote_control_controller_websocket`.", - "items": { - "type": "string" - }, - "type": "array" - }, - "sessionId": { - "description": "Backend-issued websocket session id that this proof authorizes.", - "type": "string" - }, - "targetOrigin": { - "description": "Origin of the backend endpoint that issued the challenge and will verify this proof.", - "type": "string" - }, - "targetPath": { - "description": "Websocket route path that this proof authorizes.", - "type": "string" - }, - "tokenExpiresAt": { - "description": "Remote-control token expiration as Unix seconds.", - "format": "int64", - "type": "integer" - }, - "tokenSha256Base64url": { - "description": "SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url.", - "type": "string" - }, - "type": { - "enum": [ - "remoteControlClientConnection" - ], - "title": "RemoteControlClientConnectionDeviceKeySignPayloadType", - "type": "string" - } - }, - "required": [ - "accountUserId", - "audience", - "clientId", - "nonce", - "scopes", - "sessionId", - "targetOrigin", - "targetPath", - "tokenExpiresAt", - "tokenSha256Base64url", - "type" - ], - "title": "RemoteControlClientConnectionDeviceKeySignPayload", - "type": "object" - }, - { - "description": "Payload bound to a remote-control client `/client/enroll` ownership challenge.", - "properties": { - "accountUserId": { - "type": "string" - }, - "audience": { - "$ref": "#/definitions/RemoteControlClientEnrollmentAudience" - }, - "challengeExpiresAt": { - "description": "Enrollment challenge expiration as Unix seconds.", - "format": "int64", - "type": "integer" - }, - "challengeId": { - "description": "Backend-issued enrollment challenge id that this proof authorizes.", - "type": "string" - }, - "clientId": { - "type": "string" - }, - "deviceIdentitySha256Base64url": { - "description": "SHA-256 of the requested device identity operation, encoded as unpadded base64url.", - "type": "string" - }, - "nonce": { - "type": "string" - }, - "targetOrigin": { - "description": "Origin of the backend endpoint that issued the challenge and will verify this proof.", - "type": "string" - }, - "targetPath": { - "description": "HTTP route path that this proof authorizes.", - "type": "string" - }, - "type": { - "enum": [ - "remoteControlClientEnrollment" - ], - "title": "RemoteControlClientEnrollmentDeviceKeySignPayloadType", - "type": "string" - } - }, - "required": [ - "accountUserId", - "audience", - "challengeExpiresAt", - "challengeId", - "clientId", - "deviceIdentitySha256Base64url", - "nonce", - "targetOrigin", - "targetPath", - "type" - ], - "title": "RemoteControlClientEnrollmentDeviceKeySignPayload", - "type": "object" - } - ] - }, - "DeviceKeySignResponse": { - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "ASN.1 DER signature returned by `device/key/sign`.", - "properties": { - "algorithm": { - "$ref": "#/definitions/DeviceKeyAlgorithm" - }, - "signatureDerBase64": { - "description": "ECDSA signature DER encoded as base64.", - "type": "string" - }, - "signedPayloadBase64": { - "description": "Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte string directly and must not reserialize `payload`.", - "type": "string" - } - }, - "required": [ - "algorithm", - "signatureDerBase64", - "signedPayloadBase64" - ], - "title": "DeviceKeySignResponse", - "type": "object" - }, "DynamicToolCallOutputContentItem": { "oneOf": [ { @@ -10184,20 +9818,6 @@ "title": "ReasoningTextDeltaNotification", "type": "object" }, - "RemoteControlClientConnectionAudience": { - "description": "Audience for a remote-control client connection device-key proof.", - "enum": [ - "remote_control_client_websocket" - ], - "type": "string" - }, - "RemoteControlClientEnrollmentAudience": { - "description": "Audience for a remote-control client enrollment device-key proof.", - "enum": [ - "remote_control_client_enrollment" - ], - "type": "string" - }, "RemoteControlConnectionStatus": { "enum": [ "disabled", @@ -16617,4 +16237,4 @@ }, "title": "CodexAppServerProtocolV2", "type": "object" -} \ No newline at end of file +} diff --git a/codex-rs/app-server-protocol/schema/json/v2/DeviceKeyCreateParams.json b/codex-rs/app-server-protocol/schema/json/v2/DeviceKeyCreateParams.json deleted file mode 100644 index fe2c0f089576..000000000000 --- a/codex-rs/app-server-protocol/schema/json/v2/DeviceKeyCreateParams.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "DeviceKeyProtectionPolicy": { - "description": "Protection policy for creating or loading a controller-local device key.", - "enum": [ - "hardware_only", - "allow_os_protected_nonextractable" - ], - "type": "string" - } - }, - "description": "Create a controller-local device key with a random key id.", - "properties": { - "accountUserId": { - "type": "string" - }, - "clientId": { - "type": "string" - }, - "protectionPolicy": { - "anyOf": [ - { - "$ref": "#/definitions/DeviceKeyProtectionPolicy" - }, - { - "type": "null" - } - ], - "description": "Defaults to `hardware_only` when omitted." - } - }, - "required": [ - "accountUserId", - "clientId" - ], - "title": "DeviceKeyCreateParams", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/DeviceKeyCreateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/DeviceKeyCreateResponse.json deleted file mode 100644 index 12072588a998..000000000000 --- a/codex-rs/app-server-protocol/schema/json/v2/DeviceKeyCreateResponse.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "DeviceKeyAlgorithm": { - "description": "Device-key algorithm reported at enrollment and signing boundaries.", - "enum": [ - "ecdsa_p256_sha256" - ], - "type": "string" - }, - "DeviceKeyProtectionClass": { - "description": "Platform protection class for a controller-local device key.", - "enum": [ - "hardware_secure_enclave", - "hardware_tpm", - "os_protected_nonextractable" - ], - "type": "string" - } - }, - "description": "Device-key metadata and public key returned by create/public APIs.", - "properties": { - "algorithm": { - "$ref": "#/definitions/DeviceKeyAlgorithm" - }, - "keyId": { - "type": "string" - }, - "protectionClass": { - "$ref": "#/definitions/DeviceKeyProtectionClass" - }, - "publicKeySpkiDerBase64": { - "description": "SubjectPublicKeyInfo DER encoded as base64.", - "type": "string" - } - }, - "required": [ - "algorithm", - "keyId", - "protectionClass", - "publicKeySpkiDerBase64" - ], - "title": "DeviceKeyCreateResponse", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/DeviceKeyPublicParams.json b/codex-rs/app-server-protocol/schema/json/v2/DeviceKeyPublicParams.json deleted file mode 100644 index 37cc5fbe2c16..000000000000 --- a/codex-rs/app-server-protocol/schema/json/v2/DeviceKeyPublicParams.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "description": "Fetch a controller-local device key public key by id.", - "properties": { - "keyId": { - "type": "string" - } - }, - "required": [ - "keyId" - ], - "title": "DeviceKeyPublicParams", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/DeviceKeyPublicResponse.json b/codex-rs/app-server-protocol/schema/json/v2/DeviceKeyPublicResponse.json deleted file mode 100644 index 39f98b7623f8..000000000000 --- a/codex-rs/app-server-protocol/schema/json/v2/DeviceKeyPublicResponse.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "DeviceKeyAlgorithm": { - "description": "Device-key algorithm reported at enrollment and signing boundaries.", - "enum": [ - "ecdsa_p256_sha256" - ], - "type": "string" - }, - "DeviceKeyProtectionClass": { - "description": "Platform protection class for a controller-local device key.", - "enum": [ - "hardware_secure_enclave", - "hardware_tpm", - "os_protected_nonextractable" - ], - "type": "string" - } - }, - "description": "Device-key public metadata returned by `device/key/public`.", - "properties": { - "algorithm": { - "$ref": "#/definitions/DeviceKeyAlgorithm" - }, - "keyId": { - "type": "string" - }, - "protectionClass": { - "$ref": "#/definitions/DeviceKeyProtectionClass" - }, - "publicKeySpkiDerBase64": { - "description": "SubjectPublicKeyInfo DER encoded as base64.", - "type": "string" - } - }, - "required": [ - "algorithm", - "keyId", - "protectionClass", - "publicKeySpkiDerBase64" - ], - "title": "DeviceKeyPublicResponse", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/DeviceKeySignParams.json b/codex-rs/app-server-protocol/schema/json/v2/DeviceKeySignParams.json deleted file mode 100644 index 054765b2974b..000000000000 --- a/codex-rs/app-server-protocol/schema/json/v2/DeviceKeySignParams.json +++ /dev/null @@ -1,165 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "DeviceKeySignPayload": { - "description": "Structured payloads accepted by `device/key/sign`.", - "oneOf": [ - { - "description": "Payload bound to one remote-control controller websocket `/client` connection challenge.", - "properties": { - "accountUserId": { - "type": "string" - }, - "audience": { - "$ref": "#/definitions/RemoteControlClientConnectionAudience" - }, - "clientId": { - "type": "string" - }, - "nonce": { - "type": "string" - }, - "scopes": { - "description": "Must contain exactly `remote_control_controller_websocket`.", - "items": { - "type": "string" - }, - "type": "array" - }, - "sessionId": { - "description": "Backend-issued websocket session id that this proof authorizes.", - "type": "string" - }, - "targetOrigin": { - "description": "Origin of the backend endpoint that issued the challenge and will verify this proof.", - "type": "string" - }, - "targetPath": { - "description": "Websocket route path that this proof authorizes.", - "type": "string" - }, - "tokenExpiresAt": { - "description": "Remote-control token expiration as Unix seconds.", - "format": "int64", - "type": "integer" - }, - "tokenSha256Base64url": { - "description": "SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url.", - "type": "string" - }, - "type": { - "enum": [ - "remoteControlClientConnection" - ], - "title": "RemoteControlClientConnectionDeviceKeySignPayloadType", - "type": "string" - } - }, - "required": [ - "accountUserId", - "audience", - "clientId", - "nonce", - "scopes", - "sessionId", - "targetOrigin", - "targetPath", - "tokenExpiresAt", - "tokenSha256Base64url", - "type" - ], - "title": "RemoteControlClientConnectionDeviceKeySignPayload", - "type": "object" - }, - { - "description": "Payload bound to a remote-control client `/client/enroll` ownership challenge.", - "properties": { - "accountUserId": { - "type": "string" - }, - "audience": { - "$ref": "#/definitions/RemoteControlClientEnrollmentAudience" - }, - "challengeExpiresAt": { - "description": "Enrollment challenge expiration as Unix seconds.", - "format": "int64", - "type": "integer" - }, - "challengeId": { - "description": "Backend-issued enrollment challenge id that this proof authorizes.", - "type": "string" - }, - "clientId": { - "type": "string" - }, - "deviceIdentitySha256Base64url": { - "description": "SHA-256 of the requested device identity operation, encoded as unpadded base64url.", - "type": "string" - }, - "nonce": { - "type": "string" - }, - "targetOrigin": { - "description": "Origin of the backend endpoint that issued the challenge and will verify this proof.", - "type": "string" - }, - "targetPath": { - "description": "HTTP route path that this proof authorizes.", - "type": "string" - }, - "type": { - "enum": [ - "remoteControlClientEnrollment" - ], - "title": "RemoteControlClientEnrollmentDeviceKeySignPayloadType", - "type": "string" - } - }, - "required": [ - "accountUserId", - "audience", - "challengeExpiresAt", - "challengeId", - "clientId", - "deviceIdentitySha256Base64url", - "nonce", - "targetOrigin", - "targetPath", - "type" - ], - "title": "RemoteControlClientEnrollmentDeviceKeySignPayload", - "type": "object" - } - ] - }, - "RemoteControlClientConnectionAudience": { - "description": "Audience for a remote-control client connection device-key proof.", - "enum": [ - "remote_control_client_websocket" - ], - "type": "string" - }, - "RemoteControlClientEnrollmentAudience": { - "description": "Audience for a remote-control client enrollment device-key proof.", - "enum": [ - "remote_control_client_enrollment" - ], - "type": "string" - } - }, - "description": "Sign an accepted structured payload with a controller-local device key.", - "properties": { - "keyId": { - "type": "string" - }, - "payload": { - "$ref": "#/definitions/DeviceKeySignPayload" - } - }, - "required": [ - "keyId", - "payload" - ], - "title": "DeviceKeySignParams", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/json/v2/DeviceKeySignResponse.json b/codex-rs/app-server-protocol/schema/json/v2/DeviceKeySignResponse.json deleted file mode 100644 index 83fec90330ed..000000000000 --- a/codex-rs/app-server-protocol/schema/json/v2/DeviceKeySignResponse.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "DeviceKeyAlgorithm": { - "description": "Device-key algorithm reported at enrollment and signing boundaries.", - "enum": [ - "ecdsa_p256_sha256" - ], - "type": "string" - } - }, - "description": "ASN.1 DER signature returned by `device/key/sign`.", - "properties": { - "algorithm": { - "$ref": "#/definitions/DeviceKeyAlgorithm" - }, - "signatureDerBase64": { - "description": "ECDSA signature DER encoded as base64.", - "type": "string" - }, - "signedPayloadBase64": { - "description": "Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte string directly and must not reserialize `payload`.", - "type": "string" - } - }, - "required": [ - "algorithm", - "signatureDerBase64", - "signedPayloadBase64" - ], - "title": "DeviceKeySignResponse", - "type": "object" -} \ No newline at end of file diff --git a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts index 1c03d2eb3715..a12185b50103 100644 --- a/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts +++ b/codex-rs/app-server-protocol/schema/typescript/ClientRequest.ts @@ -16,9 +16,6 @@ import type { CommandExecWriteParams } from "./v2/CommandExecWriteParams"; import type { ConfigBatchWriteParams } from "./v2/ConfigBatchWriteParams"; import type { ConfigReadParams } from "./v2/ConfigReadParams"; import type { ConfigValueWriteParams } from "./v2/ConfigValueWriteParams"; -import type { DeviceKeyCreateParams } from "./v2/DeviceKeyCreateParams"; -import type { DeviceKeyPublicParams } from "./v2/DeviceKeyPublicParams"; -import type { DeviceKeySignParams } from "./v2/DeviceKeySignParams"; import type { ExperimentalFeatureEnablementSetParams } from "./v2/ExperimentalFeatureEnablementSetParams"; import type { ExperimentalFeatureListParams } from "./v2/ExperimentalFeatureListParams"; import type { ExternalAgentConfigDetectParams } from "./v2/ExternalAgentConfigDetectParams"; @@ -82,4 +79,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/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "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": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "method": "app/list", id: RequestId, params: AppsListParams, } | { "method": "device/key/create", id: RequestId, params: DeviceKeyCreateParams, } | { "method": "device/key/public", id: RequestId, params: DeviceKeyPublicParams, } | { "method": "device/key/sign", id: RequestId, params: DeviceKeySignParams, } | { "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": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "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": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "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": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "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/approveGuardianDeniedAction", id: RequestId, params: ThreadApproveGuardianDeniedActionParams, } | { "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": "thread/inject_items", id: RequestId, params: ThreadInjectItemsParams, } | { "method": "skills/list", id: RequestId, params: SkillsListParams, } | { "method": "hooks/list", id: RequestId, params: HooksListParams, } | { "method": "marketplace/add", id: RequestId, params: MarketplaceAddParams, } | { "method": "marketplace/remove", id: RequestId, params: MarketplaceRemoveParams, } | { "method": "marketplace/upgrade", id: RequestId, params: MarketplaceUpgradeParams, } | { "method": "plugin/list", id: RequestId, params: PluginListParams, } | { "method": "plugin/read", id: RequestId, params: PluginReadParams, } | { "method": "plugin/skill/read", id: RequestId, params: PluginSkillReadParams, } | { "method": "plugin/share/save", id: RequestId, params: PluginShareSaveParams, } | { "method": "plugin/share/updateTargets", id: RequestId, params: PluginShareUpdateTargetsParams, } | { "method": "plugin/share/list", id: RequestId, params: PluginShareListParams, } | { "method": "plugin/share/delete", id: RequestId, params: PluginShareDeleteParams, } | { "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": "fs/watch", id: RequestId, params: FsWatchParams, } | { "method": "fs/unwatch", id: RequestId, params: FsUnwatchParams, } | { "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": "modelProvider/capabilities/read", id: RequestId, params: ModelProviderCapabilitiesReadParams, } | { "method": "experimentalFeature/list", id: RequestId, params: ExperimentalFeatureListParams, } | { "method": "experimentalFeature/enablement/set", id: RequestId, params: ExperimentalFeatureEnablementSetParams, } | { "method": "mcpServer/oauth/login", id: RequestId, params: McpServerOauthLoginParams, } | { "method": "config/mcpServer/reload", id: RequestId, params: undefined, } | { "method": "mcpServerStatus/list", id: RequestId, params: ListMcpServerStatusParams, } | { "method": "mcpServer/resource/read", id: RequestId, params: McpResourceReadParams, } | { "method": "mcpServer/tool/call", id: RequestId, params: McpServerToolCallParams, } | { "method": "windowsSandbox/setupStart", id: RequestId, params: WindowsSandboxSetupStartParams, } | { "method": "windowsSandbox/readiness", id: RequestId, params: undefined, } | { "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": "account/sendAddCreditsNudgeEmail", id: RequestId, params: SendAddCreditsNudgeEmailParams, } | { "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, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyAlgorithm.ts b/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyAlgorithm.ts deleted file mode 100644 index 6809c41eb548..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyAlgorithm.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 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. - -/** - * Device-key algorithm reported at enrollment and signing boundaries. - */ -export type DeviceKeyAlgorithm = "ecdsa_p256_sha256"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyCreateParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyCreateParams.ts deleted file mode 100644 index 7ffd9b5fa353..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyCreateParams.ts +++ /dev/null @@ -1,13 +0,0 @@ -// 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 { DeviceKeyProtectionPolicy } from "./DeviceKeyProtectionPolicy"; - -/** - * Create a controller-local device key with a random key id. - */ -export type DeviceKeyCreateParams = { -/** - * Defaults to `hardware_only` when omitted. - */ -protectionPolicy?: DeviceKeyProtectionPolicy | null, accountUserId: string, clientId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyCreateResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyCreateResponse.ts deleted file mode 100644 index 6ace37934a04..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyCreateResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -// 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 { DeviceKeyAlgorithm } from "./DeviceKeyAlgorithm"; -import type { DeviceKeyProtectionClass } from "./DeviceKeyProtectionClass"; - -/** - * Device-key metadata and public key returned by create/public APIs. - */ -export type DeviceKeyCreateResponse = { keyId: string, -/** - * SubjectPublicKeyInfo DER encoded as base64. - */ -publicKeySpkiDerBase64: string, algorithm: DeviceKeyAlgorithm, protectionClass: DeviceKeyProtectionClass, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyProtectionClass.ts b/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyProtectionClass.ts deleted file mode 100644 index ba7ff311ade2..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyProtectionClass.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 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. - -/** - * Platform protection class for a controller-local device key. - */ -export type DeviceKeyProtectionClass = "hardware_secure_enclave" | "hardware_tpm" | "os_protected_nonextractable"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyProtectionPolicy.ts b/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyProtectionPolicy.ts deleted file mode 100644 index 66fceafb514d..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyProtectionPolicy.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 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. - -/** - * Protection policy for creating or loading a controller-local device key. - */ -export type DeviceKeyProtectionPolicy = "hardware_only" | "allow_os_protected_nonextractable"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyPublicParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyPublicParams.ts deleted file mode 100644 index 5a5b77899d36..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyPublicParams.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 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. - -/** - * Fetch a controller-local device key public key by id. - */ -export type DeviceKeyPublicParams = { keyId: string, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyPublicResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyPublicResponse.ts deleted file mode 100644 index 9967c0936ee3..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeyPublicResponse.ts +++ /dev/null @@ -1,14 +0,0 @@ -// 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 { DeviceKeyAlgorithm } from "./DeviceKeyAlgorithm"; -import type { DeviceKeyProtectionClass } from "./DeviceKeyProtectionClass"; - -/** - * Device-key public metadata returned by `device/key/public`. - */ -export type DeviceKeyPublicResponse = { keyId: string, -/** - * SubjectPublicKeyInfo DER encoded as base64. - */ -publicKeySpkiDerBase64: string, algorithm: DeviceKeyAlgorithm, protectionClass: DeviceKeyProtectionClass, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeySignParams.ts b/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeySignParams.ts deleted file mode 100644 index 0886e45d9379..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeySignParams.ts +++ /dev/null @@ -1,9 +0,0 @@ -// 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 { DeviceKeySignPayload } from "./DeviceKeySignPayload"; - -/** - * Sign an accepted structured payload with a controller-local device key. - */ -export type DeviceKeySignParams = { keyId: string, payload: DeviceKeySignPayload, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeySignPayload.ts b/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeySignPayload.ts deleted file mode 100644 index 859644549037..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeySignPayload.ts +++ /dev/null @@ -1,54 +0,0 @@ -// 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 { RemoteControlClientConnectionAudience } from "./RemoteControlClientConnectionAudience"; -import type { RemoteControlClientEnrollmentAudience } from "./RemoteControlClientEnrollmentAudience"; - -/** - * Structured payloads accepted by `device/key/sign`. - */ -export type DeviceKeySignPayload = { "type": "remoteControlClientConnection", nonce: string, audience: RemoteControlClientConnectionAudience, -/** - * Backend-issued websocket session id that this proof authorizes. - */ -sessionId: string, -/** - * Origin of the backend endpoint that issued the challenge and will verify this proof. - */ -targetOrigin: string, -/** - * Websocket route path that this proof authorizes. - */ -targetPath: string, accountUserId: string, clientId: string, -/** - * Remote-control token expiration as Unix seconds. - */ -tokenExpiresAt: number, -/** - * SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url. - */ -tokenSha256Base64url: string, -/** - * Must contain exactly `remote_control_controller_websocket`. - */ -scopes: Array, } | { "type": "remoteControlClientEnrollment", nonce: string, audience: RemoteControlClientEnrollmentAudience, -/** - * Backend-issued enrollment challenge id that this proof authorizes. - */ -challengeId: string, -/** - * Origin of the backend endpoint that issued the challenge and will verify this proof. - */ -targetOrigin: string, -/** - * HTTP route path that this proof authorizes. - */ -targetPath: string, accountUserId: string, clientId: string, -/** - * SHA-256 of the requested device identity operation, encoded as unpadded base64url. - */ -deviceIdentitySha256Base64url: string, -/** - * Enrollment challenge expiration as Unix seconds. - */ -challengeExpiresAt: number, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeySignResponse.ts b/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeySignResponse.ts deleted file mode 100644 index cf77fae27f48..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/DeviceKeySignResponse.ts +++ /dev/null @@ -1,18 +0,0 @@ -// 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 { DeviceKeyAlgorithm } from "./DeviceKeyAlgorithm"; - -/** - * ASN.1 DER signature returned by `device/key/sign`. - */ -export type DeviceKeySignResponse = { -/** - * ECDSA signature DER encoded as base64. - */ -signatureDerBase64: string, -/** - * Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte - * string directly and must not reserialize `payload`. - */ -signedPayloadBase64: string, algorithm: DeviceKeyAlgorithm, }; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RemoteControlClientConnectionAudience.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RemoteControlClientConnectionAudience.ts deleted file mode 100644 index e4d41ff4c238..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/RemoteControlClientConnectionAudience.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 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. - -/** - * Audience for a remote-control client connection device-key proof. - */ -export type RemoteControlClientConnectionAudience = "remote_control_client_websocket"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/RemoteControlClientEnrollmentAudience.ts b/codex-rs/app-server-protocol/schema/typescript/v2/RemoteControlClientEnrollmentAudience.ts deleted file mode 100644 index b65fb3d11ba8..000000000000 --- a/codex-rs/app-server-protocol/schema/typescript/v2/RemoteControlClientEnrollmentAudience.ts +++ /dev/null @@ -1,8 +0,0 @@ -// 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. - -/** - * Audience for a remote-control client enrollment device-key proof. - */ -export type RemoteControlClientEnrollmentAudience = "remote_control_client_enrollment"; diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts index e624d704e69d..5940c929048b 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/index.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/index.ts @@ -79,16 +79,6 @@ export type { ConfiguredHookMatcherGroup } from "./ConfiguredHookMatcherGroup"; export type { ContextCompactedNotification } from "./ContextCompactedNotification"; export type { CreditsSnapshot } from "./CreditsSnapshot"; export type { DeprecationNoticeNotification } from "./DeprecationNoticeNotification"; -export type { DeviceKeyAlgorithm } from "./DeviceKeyAlgorithm"; -export type { DeviceKeyCreateParams } from "./DeviceKeyCreateParams"; -export type { DeviceKeyCreateResponse } from "./DeviceKeyCreateResponse"; -export type { DeviceKeyProtectionClass } from "./DeviceKeyProtectionClass"; -export type { DeviceKeyProtectionPolicy } from "./DeviceKeyProtectionPolicy"; -export type { DeviceKeyPublicParams } from "./DeviceKeyPublicParams"; -export type { DeviceKeyPublicResponse } from "./DeviceKeyPublicResponse"; -export type { DeviceKeySignParams } from "./DeviceKeySignParams"; -export type { DeviceKeySignPayload } from "./DeviceKeySignPayload"; -export type { DeviceKeySignResponse } from "./DeviceKeySignResponse"; export type { DynamicToolCallOutputContentItem } from "./DynamicToolCallOutputContentItem"; export type { DynamicToolCallParams } from "./DynamicToolCallParams"; export type { DynamicToolCallResponse } from "./DynamicToolCallResponse"; @@ -318,8 +308,6 @@ export type { ReasoningEffortOption } from "./ReasoningEffortOption"; export type { ReasoningSummaryPartAddedNotification } from "./ReasoningSummaryPartAddedNotification"; export type { ReasoningSummaryTextDeltaNotification } from "./ReasoningSummaryTextDeltaNotification"; export type { ReasoningTextDeltaNotification } from "./ReasoningTextDeltaNotification"; -export type { RemoteControlClientConnectionAudience } from "./RemoteControlClientConnectionAudience"; -export type { RemoteControlClientEnrollmentAudience } from "./RemoteControlClientEnrollmentAudience"; export type { RemoteControlConnectionStatus } from "./RemoteControlConnectionStatus"; export type { RemoteControlStatusChangedNotification } from "./RemoteControlStatusChangedNotification"; export type { RequestPermissionProfile } from "./RequestPermissionProfile"; diff --git a/codex-rs/app-server-protocol/src/protocol/common.rs b/codex-rs/app-server-protocol/src/protocol/common.rs index d687a79ec945..e79c99a9c971 100644 --- a/codex-rs/app-server-protocol/src/protocol/common.rs +++ b/codex-rs/app-server-protocol/src/protocol/common.rs @@ -652,21 +652,6 @@ client_request_definitions! { serialization: None, response: v2::AppsListResponse, }, - DeviceKeyCreate => "device/key/create" { - params: v2::DeviceKeyCreateParams, - serialization: global("device-key"), - response: v2::DeviceKeyCreateResponse, - }, - DeviceKeyPublic => "device/key/public" { - params: v2::DeviceKeyPublicParams, - serialization: global("device-key"), - response: v2::DeviceKeyPublicResponse, - }, - DeviceKeySign => "device/key/sign" { - params: v2::DeviceKeySignParams, - serialization: global("device-key"), - response: v2::DeviceKeySignResponse, - }, // File system requests are intentionally concurrent. Desktop already treats local // file system operations as concurrent, and app-server remote fs mirrors that model. FsReadFile => "fs/readFile" { @@ -1789,19 +1774,6 @@ mod tests { Some(ClientRequestSerializationScope::Global("config")) ); - let device_key_create = ClientRequest::DeviceKeyCreate { - request_id: request_id(), - params: v2::DeviceKeyCreateParams { - protection_policy: None, - account_user_id: "user".to_string(), - client_id: "client".to_string(), - }, - }; - assert_eq!( - device_key_create.serialization_scope(), - Some(ClientRequestSerializationScope::Global("device-key")) - ); - let add_credits_nudge = ClientRequest::SendAddCreditsNudgeEmail { request_id: request_id(), params: v2::SendAddCreditsNudgeEmailParams { diff --git a/codex-rs/app-server-protocol/src/protocol/v2/device_key.rs b/codex-rs/app-server-protocol/src/protocol/v2/device_key.rs deleted file mode 100644 index 3330996c1c99..000000000000 --- a/codex-rs/app-server-protocol/src/protocol/v2/device_key.rs +++ /dev/null @@ -1,181 +0,0 @@ -use schemars::JsonSchema; -use serde::Deserialize; -use serde::Serialize; -use ts_rs::TS; - -/// Device-key algorithm reported at enrollment and signing boundaries. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(rename_all = "snake_case", export_to = "v2/")] -pub enum DeviceKeyAlgorithm { - EcdsaP256Sha256, -} - -/// Platform protection class for a controller-local device key. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(rename_all = "snake_case", export_to = "v2/")] -pub enum DeviceKeyProtectionClass { - HardwareSecureEnclave, - HardwareTpm, - OsProtectedNonextractable, -} - -/// Protection policy for creating or loading a controller-local device key. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(rename_all = "snake_case", export_to = "v2/")] -pub enum DeviceKeyProtectionPolicy { - HardwareOnly, - AllowOsProtectedNonextractable, -} - -/// Create a controller-local device key with a random key id. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DeviceKeyCreateParams { - /// Defaults to `hardware_only` when omitted. - #[ts(optional = nullable)] - pub protection_policy: Option, - pub account_user_id: String, - pub client_id: String, -} - -/// Device-key metadata and public key returned by create/public APIs. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DeviceKeyCreateResponse { - pub key_id: String, - /// SubjectPublicKeyInfo DER encoded as base64. - pub public_key_spki_der_base64: String, - pub algorithm: DeviceKeyAlgorithm, - pub protection_class: DeviceKeyProtectionClass, -} - -/// Fetch a controller-local device key public key by id. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DeviceKeyPublicParams { - pub key_id: String, -} - -/// Device-key public metadata returned by `device/key/public`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DeviceKeyPublicResponse { - pub key_id: String, - /// SubjectPublicKeyInfo DER encoded as base64. - pub public_key_spki_der_base64: String, - pub algorithm: DeviceKeyAlgorithm, - pub protection_class: DeviceKeyProtectionClass, -} - -/// Current remote-control connection status and environment id exposed to clients. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct RemoteControlStatusChangedNotification { - pub status: RemoteControlConnectionStatus, - pub environment_id: Option, -} - -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(rename_all = "camelCase", export_to = "v2/")] -pub enum RemoteControlConnectionStatus { - Disabled, - Connecting, - Connected, - Errored, -} - -/// Audience for a remote-control client connection device-key proof. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(rename_all = "snake_case", export_to = "v2/")] -pub enum RemoteControlClientConnectionAudience { - RemoteControlClientWebsocket, -} - -/// Audience for a remote-control client enrollment device-key proof. -#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "snake_case")] -#[ts(rename_all = "snake_case", export_to = "v2/")] -pub enum RemoteControlClientEnrollmentAudience { - RemoteControlClientEnrollment, -} - -/// Structured payloads accepted by `device/key/sign`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(tag = "type", rename_all = "camelCase")] -#[ts(tag = "type", export_to = "v2/")] -pub enum DeviceKeySignPayload { - /// Payload bound to one remote-control controller websocket `/client` connection challenge. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - RemoteControlClientConnection { - nonce: String, - audience: RemoteControlClientConnectionAudience, - /// Backend-issued websocket session id that this proof authorizes. - session_id: String, - /// Origin of the backend endpoint that issued the challenge and will verify this proof. - target_origin: String, - /// Websocket route path that this proof authorizes. - target_path: String, - account_user_id: String, - client_id: String, - /// Remote-control token expiration as Unix seconds. - #[ts(type = "number")] - token_expires_at: i64, - /// SHA-256 of the controller-scoped remote-control token, encoded as unpadded base64url. - token_sha256_base64url: String, - /// Must contain exactly `remote_control_controller_websocket`. - scopes: Vec, - }, - /// Payload bound to a remote-control client `/client/enroll` ownership challenge. - #[serde(rename_all = "camelCase")] - #[ts(rename_all = "camelCase")] - RemoteControlClientEnrollment { - nonce: String, - audience: RemoteControlClientEnrollmentAudience, - /// Backend-issued enrollment challenge id that this proof authorizes. - challenge_id: String, - /// Origin of the backend endpoint that issued the challenge and will verify this proof. - target_origin: String, - /// HTTP route path that this proof authorizes. - target_path: String, - account_user_id: String, - client_id: String, - /// SHA-256 of the requested device identity operation, encoded as unpadded base64url. - device_identity_sha256_base64url: String, - /// Enrollment challenge expiration as Unix seconds. - #[ts(type = "number")] - challenge_expires_at: i64, - }, -} - -/// Sign an accepted structured payload with a controller-local device key. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DeviceKeySignParams { - pub key_id: String, - pub payload: DeviceKeySignPayload, -} - -/// ASN.1 DER signature returned by `device/key/sign`. -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] -#[serde(rename_all = "camelCase")] -#[ts(export_to = "v2/")] -pub struct DeviceKeySignResponse { - /// ECDSA signature DER encoded as base64. - pub signature_der_base64: String, - /// Exact bytes signed by the device key, encoded as base64. Verifiers must verify this byte - /// string directly and must not reserialize `payload`. - pub signed_payload_base64: String, - pub algorithm: DeviceKeyAlgorithm, -} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/mod.rs b/codex-rs/app-server-protocol/src/protocol/v2/mod.rs index df8a363f827e..275e7ca45b4f 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/mod.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/mod.rs @@ -5,7 +5,6 @@ mod apps; mod collaboration_mode; mod command_exec; mod config; -mod device_key; mod experimental_feature; mod feedback; mod fs; @@ -18,6 +17,7 @@ mod permissions; mod plugin; mod process; mod realtime; +mod remote_control; mod review; mod thread; mod thread_data; @@ -29,7 +29,6 @@ pub use apps::*; pub use collaboration_mode::*; pub use command_exec::*; pub use config::*; -pub use device_key::*; pub use experimental_feature::*; pub use feedback::*; pub use fs::*; @@ -42,6 +41,7 @@ pub use permissions::*; pub use plugin::*; pub use process::*; pub use realtime::*; +pub use remote_control::*; pub use review::*; pub use shared::*; pub use thread::*; diff --git a/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs b/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs new file mode 100644 index 000000000000..7d6383f46800 --- /dev/null +++ b/codex-rs/app-server-protocol/src/protocol/v2/remote_control.rs @@ -0,0 +1,23 @@ +use schemars::JsonSchema; +use serde::Deserialize; +use serde::Serialize; +use ts_rs::TS; + +/// Current remote-control connection status and environment id exposed to clients. +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct RemoteControlStatusChangedNotification { + pub status: RemoteControlConnectionStatus, + pub environment_id: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(rename_all = "camelCase", export_to = "v2/")] +pub enum RemoteControlConnectionStatus { + Disabled, + Connecting, + Connected, + Errored, +} diff --git a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs index ba6f4e0eebcc..4e923c580405 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2/tests.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2/tests.rs @@ -664,181 +664,6 @@ fn fs_read_file_params_round_trip() { assert_eq!(decoded, params); } -#[test] -fn device_key_create_params_round_trip_uses_protection_policy() { - let params = DeviceKeyCreateParams { - protection_policy: None, - account_user_id: "account-user-1".to_string(), - client_id: "cli_123".to_string(), - }; - - let value = serde_json::to_value(¶ms).expect("serialize device/key/create params"); - assert_eq!( - value, - json!({ - "accountUserId": "account-user-1", - "clientId": "cli_123", - "protectionPolicy": null, - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize device/key/create params"); - assert_eq!(decoded, params); - - let params = DeviceKeyCreateParams { - protection_policy: Some(DeviceKeyProtectionPolicy::AllowOsProtectedNonextractable), - account_user_id: "account-user-1".to_string(), - client_id: "cli_123".to_string(), - }; - let value = serde_json::to_value(¶ms) - .expect("serialize device/key/create params with protection policy"); - assert_eq!( - value, - json!({ - "accountUserId": "account-user-1", - "clientId": "cli_123", - "protectionPolicy": "allow_os_protected_nonextractable", - }) - ); -} - -#[test] -fn device_key_create_response_round_trips_protection_class() { - let response = DeviceKeyCreateResponse { - key_id: "dk_123".to_string(), - public_key_spki_der_base64: "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE".to_string(), - algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256, - protection_class: DeviceKeyProtectionClass::OsProtectedNonextractable, - }; - - let value = serde_json::to_value(&response).expect("serialize device/key/create response"); - assert_eq!( - value, - json!({ - "keyId": "dk_123", - "publicKeySpkiDerBase64": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE", - "algorithm": "ecdsa_p256_sha256", - "protectionClass": "os_protected_nonextractable", - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize device/key/create response"); - assert_eq!(decoded, response); -} - -#[test] -fn device_key_sign_params_round_trip_uses_accepted_payload_enum() { - let params = DeviceKeySignParams { - key_id: "dk_123".to_string(), - payload: DeviceKeySignPayload::RemoteControlClientConnection { - nonce: "nonce-1".to_string(), - audience: RemoteControlClientConnectionAudience::RemoteControlClientWebsocket, - session_id: "wssess_123".to_string(), - target_origin: "https://chatgpt.com".to_string(), - target_path: "/api/codex/remote/control/client".to_string(), - account_user_id: "account-user-1".to_string(), - client_id: "cli_123".to_string(), - token_sha256_base64url: "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU".to_string(), - token_expires_at: 1_700_000_000, - scopes: vec!["remote_control_controller_websocket".to_string()], - }, - }; - - let value = serde_json::to_value(¶ms).expect("serialize device/key/sign params"); - assert_eq!( - value, - json!({ - "keyId": "dk_123", - "payload": { - "type": "remoteControlClientConnection", - "nonce": "nonce-1", - "audience": "remote_control_client_websocket", - "sessionId": "wssess_123", - "targetOrigin": "https://chatgpt.com", - "targetPath": "/api/codex/remote/control/client", - "accountUserId": "account-user-1", - "clientId": "cli_123", - "tokenSha256Base64url": "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU", - "tokenExpiresAt": 1_700_000_000, - "scopes": ["remote_control_controller_websocket"], - }, - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize device/key/sign params"); - assert_eq!(decoded, params); -} - -#[test] -fn device_key_sign_params_round_trip_uses_enrollment_payload() { - let params = DeviceKeySignParams { - key_id: "dk_123".to_string(), - payload: DeviceKeySignPayload::RemoteControlClientEnrollment { - nonce: "nonce-1".to_string(), - audience: RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment, - challenge_id: "rch_123".to_string(), - target_origin: "https://chatgpt.com".to_string(), - target_path: "/wham/remote/control/client/enroll".to_string(), - account_user_id: "account-user-1".to_string(), - client_id: "cli_123".to_string(), - device_identity_sha256_base64url: "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU" - .to_string(), - challenge_expires_at: 1_700_000_000, - }, - }; - - let value = serde_json::to_value(¶ms) - .expect("serialize device/key/sign params with enrollment payload"); - assert_eq!( - value, - json!({ - "keyId": "dk_123", - "payload": { - "type": "remoteControlClientEnrollment", - "nonce": "nonce-1", - "audience": "remote_control_client_enrollment", - "challengeId": "rch_123", - "targetOrigin": "https://chatgpt.com", - "targetPath": "/wham/remote/control/client/enroll", - "accountUserId": "account-user-1", - "clientId": "cli_123", - "deviceIdentitySha256Base64url": "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU", - "challengeExpiresAt": 1_700_000_000, - }, - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize device/key/sign params with enrollment payload"); - assert_eq!(decoded, params); -} - -#[test] -fn device_key_sign_response_returns_signed_payload_bytes() { - let response = DeviceKeySignResponse { - signature_der_base64: "MEUCIQD".to_string(), - signed_payload_base64: "eyJkb21haW4iOiJjb2RleA".to_string(), - algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256, - }; - - let value = serde_json::to_value(&response).expect("serialize device/key/sign response"); - assert_eq!( - value, - json!({ - "signatureDerBase64": "MEUCIQD", - "signedPayloadBase64": "eyJkb21haW4iOiJjb2RleA", - "algorithm": "ecdsa_p256_sha256", - }) - ); - - let decoded = serde_json::from_value::(value) - .expect("deserialize device/key/sign response"); - assert_eq!(decoded, response); -} - #[test] fn fs_create_directory_params_round_trip_with_default_recursive() { let params = FsCreateDirectoryParams { diff --git a/codex-rs/app-server-transport/src/transport/mod.rs b/codex-rs/app-server-transport/src/transport/mod.rs index e1590ab43a8a..c63a79a0c14c 100644 --- a/codex-rs/app-server-transport/src/transport/mod.rs +++ b/codex-rs/app-server-transport/src/transport/mod.rs @@ -171,14 +171,6 @@ pub enum ConnectionOrigin { RemoteControl, } -impl ConnectionOrigin { - pub fn allows_device_key_requests(self) -> bool { - // Device-key endpoints are only for local connections that own the app-server instance. - // Do not include remote transports such as SSH or remote-control websocket connections. - matches!(self, Self::Stdio | Self::InProcess) - } -} - static CONNECTION_ID_COUNTER: AtomicU64 = AtomicU64::new(0); fn next_connection_id() -> ConnectionId { diff --git a/codex-rs/app-server/Cargo.toml b/codex-rs/app-server/Cargo.toml index 6d201bdee3f1..c9031fd0ac90 100644 --- a/codex-rs/app-server/Cargo.toml +++ b/codex-rs/app-server/Cargo.toml @@ -35,7 +35,6 @@ codex-cloud-requirements = { workspace = true } codex-config = { workspace = true } codex-core = { workspace = true } codex-core-plugins = { workspace = true } -codex-device-key = { workspace = true } codex-exec-server = { workspace = true } codex-external-agent-migration = { workspace = true } codex-external-agent-sessions = { workspace = true } diff --git a/codex-rs/app-server/README.md b/codex-rs/app-server/README.md index ddc381795272..71bc9961d678 100644 --- a/codex-rs/app-server/README.md +++ b/codex-rs/app-server/README.md @@ -212,9 +212,6 @@ Example with notification opt-out: - `plugin/skill/read` — read remote plugin skill markdown on demand by `remoteMarketplaceName`, `remotePluginId`, and `skillName`. This lets clients preview uninstalled remote plugin skills without downloading the plugin bundle. - `skills/changed` — notification emitted when watched local skill files change. - `app/list` — list available apps. -- `device/key/create` — create or load a controller-local device signing key for an account/client binding. This local-key API is available only over local transports such as stdio and in-process; remote transports reject it. Hardware-backed providers are the target protection class; an OS-protected non-extractable fallback is allowed only with `protectionPolicy: "allow_os_protected_nonextractable"` and returns the reported `protectionClass`. -- `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. - `remoteControl/status/changed` — notification emitted when the remote-control status or client-visible environment id changes. `status` is one of `disabled`, `connecting`, `connected`, or `errored`; `environmentId` is a string when the app-server has a current enrollment and `null` when that enrollment is cleared, invalidated, or remote control is disabled. Newly initialized app-server clients always receive the current status snapshot. - `skills/config/write` — write user-level skill config by name or absolute path. - `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**). diff --git a/codex-rs/app-server/src/in_process.rs b/codex-rs/app-server/src/in_process.rs index dda4a3bf4a00..d812888e62a3 100644 --- a/codex-rs/app-server/src/in_process.rs +++ b/codex-rs/app-server/src/in_process.rs @@ -64,7 +64,6 @@ use crate::outgoing_message::OutgoingMessage; use crate::outgoing_message::OutgoingMessageSender; use crate::outgoing_message::QueuedOutgoingMessage; use crate::transport::CHANNEL_CAPACITY; -use crate::transport::ConnectionOrigin; use crate::transport::OutboundConnectionState; use crate::transport::route_outgoing_envelope; use codex_analytics::AppServerRpcTransport; @@ -435,7 +434,7 @@ async fn start_uninitialized(args: InProcessStartArgs) -> IoResult IoResult, initialized: OnceLock, } @@ -194,14 +190,13 @@ pub(crate) struct InitializedConnectionSessionState { impl Default for ConnectionSessionState { fn default() -> Self { - Self::new(ConnectionOrigin::WebSocket) + Self::new() } } impl ConnectionSessionState { - pub(crate) fn new(origin: ConnectionOrigin) -> Self { + pub(crate) fn new() -> Self { Self { - origin, rpc_gate: Arc::new(ConnectionRpcGate::new()), initialized: OnceLock::new(), } @@ -211,10 +206,6 @@ impl ConnectionSessionState { self.initialized.get().is_some() } - fn allows_device_key_requests(&self) -> bool { - self.origin.allows_device_key_requests() - } - pub(crate) fn experimental_api_enabled(&self) -> bool { self.initialized .get() @@ -397,7 +388,7 @@ impl MessageProcessor { thread_watch_manager.clone(), Arc::clone(&thread_list_state_permit), thread_goal_processor.clone(), - state_db.clone(), + state_db, ); let turn_processor = TurnRequestProcessor::new( auth_manager.clone(), @@ -441,7 +432,6 @@ impl MessageProcessor { arg0_paths, config.codex_home.to_path_buf(), ); - let device_key_processor = DeviceKeyRequestProcessor::new(outgoing.clone(), state_db); let fs_processor = FsRequestProcessor::new( thread_manager .environment_manager() @@ -463,7 +453,6 @@ impl MessageProcessor { command_exec_processor, process_exec_processor, config_processor, - device_key_processor, external_agent_config_processor, feedback_processor, fs_processor, @@ -770,7 +759,6 @@ impl MessageProcessor { let serialization_scope = codex_request.serialization_scope(); let app_server_client_name = session.app_server_client_name().map(str::to_string); let client_version = session.client_version().map(str::to_string); - let device_key_requests_allowed = session.allows_device_key_requests(); let error_request_id = connection_request_id.clone(); let rpc_gate = Arc::clone(&session.rpc_gate); let processor = Arc::clone(self); @@ -786,7 +774,6 @@ impl MessageProcessor { request_context, app_server_client_name, client_version, - device_key_requests_allowed, ) .await; if let Err(error) = result { @@ -816,7 +803,6 @@ impl MessageProcessor { request_context: RequestContext, app_server_client_name: Option, client_version: Option, - device_key_requests_allowed: bool, ) -> Result<(), JSONRPCErrorError> { let connection_id = connection_request_id.connection_id; let request_id = ConnectionRequestId { @@ -864,30 +850,6 @@ impl MessageProcessor { .config_requirements_read() .await .map(|response| Some(response.into())), - ClientRequest::DeviceKeyCreate { params, .. } => { - self.device_key_processor.create( - request_id.clone(), - params, - device_key_requests_allowed, - ); - Ok(None) - } - ClientRequest::DeviceKeyPublic { params, .. } => { - self.device_key_processor.public( - request_id.clone(), - params, - device_key_requests_allowed, - ); - Ok(None) - } - ClientRequest::DeviceKeySign { params, .. } => { - self.device_key_processor.sign( - request_id.clone(), - params, - device_key_requests_allowed, - ); - Ok(None) - } ClientRequest::FsReadFile { params, .. } => self .fs_processor .read_file(params) diff --git a/codex-rs/app-server/src/message_processor_tracing_tests.rs b/codex-rs/app-server/src/message_processor_tracing_tests.rs index 0d4ef8279b0b..516e0423011b 100644 --- a/codex-rs/app-server/src/message_processor_tracing_tests.rs +++ b/codex-rs/app-server/src/message_processor_tracing_tests.rs @@ -6,21 +6,16 @@ use crate::config_manager::ConfigManager; use crate::outgoing_message::ConnectionId; use crate::outgoing_message::OutgoingMessageSender; use crate::transport::AppServerTransport; -use crate::transport::ConnectionOrigin; use anyhow::Result; use app_test_support::create_mock_responses_server_repeating_assistant; use app_test_support::write_mock_responses_config_toml; use codex_analytics::AppServerRpcTransport; use codex_app_server_protocol::ClientInfo; use codex_app_server_protocol::ClientRequest; -use codex_app_server_protocol::DeviceKeySignParams; -use codex_app_server_protocol::DeviceKeySignPayload; use codex_app_server_protocol::InitializeCapabilities; use codex_app_server_protocol::InitializeParams; use codex_app_server_protocol::InitializeResponse; -use codex_app_server_protocol::JSONRPCErrorError; use codex_app_server_protocol::JSONRPCRequest; -use codex_app_server_protocol::RemoteControlClientConnectionAudience; use codex_app_server_protocol::RequestId; use codex_app_server_protocol::ThreadStartParams; use codex_app_server_protocol::ThreadStartResponse; @@ -121,10 +116,6 @@ struct TracingHarness { impl TracingHarness { async fn new() -> Result { - Self::new_with_origin(ConnectionOrigin::WebSocket).await - } - - async fn new_with_origin(origin: ConnectionOrigin) -> Result { let server = create_mock_responses_server_repeating_assistant("Done").await; let codex_home = TempDir::new()?; let config = Arc::new(build_test_config(codex_home.path(), &server.uri()).await?); @@ -137,7 +128,7 @@ impl TracingHarness { _codex_home: codex_home, processor, outgoing_rx, - session: Arc::new(ConnectionSessionState::new(origin)), + session: Arc::new(ConnectionSessionState::new()), tracing, }; @@ -196,29 +187,6 @@ impl TracingHarness { read_response(&mut self.outgoing_rx, request_id).await } - async fn request_error( - &mut self, - request: ClientRequest, - trace: Option, - ) -> JSONRPCErrorError { - let request_id = match request.id() { - RequestId::Integer(request_id) => *request_id, - request_id => panic!("expected integer request id in test harness, got {request_id:?}"), - }; - let mut request = request_from_client_request(request); - request.trace = trace; - - self.processor - .process_request( - TEST_CONNECTION_ID, - request, - &AppServerTransport::Stdio, - Arc::clone(&self.session), - ) - .await; - read_error(&mut self.outgoing_rx, request_id).await - } - async fn start_thread( &mut self, request_id: i64, @@ -485,36 +453,6 @@ async fn read_response( } } -async fn read_error( - outgoing_rx: &mut mpsc::Receiver, - request_id: i64, -) -> JSONRPCErrorError { - loop { - let envelope = tokio::time::timeout(std::time::Duration::from_secs(5), outgoing_rx.recv()) - .await - .expect("timed out waiting for error") - .expect("outgoing channel closed"); - let crate::outgoing_message::OutgoingEnvelope::ToConnection { - connection_id, - message, - .. - } = envelope - else { - continue; - }; - if connection_id != TEST_CONNECTION_ID { - continue; - } - let crate::outgoing_message::OutgoingMessage::Error(error) = message else { - continue; - }; - if error.id != RequestId::Integer(request_id) { - continue; - } - return error.error; - } -} - async fn read_thread_started_notification( outgoing_rx: &mut mpsc::Receiver, ) { @@ -693,47 +631,6 @@ fn thread_start_jsonrpc_span_exports_server_span_and_parents_children() -> Resul ) } -#[tokio::test(flavor = "current_thread")] -#[serial(app_server_tracing)] -async fn remote_control_origin_rejects_device_key_requests() -> Result<()> { - let mut harness = TracingHarness::new_with_origin(ConnectionOrigin::RemoteControl).await?; - - let error = harness - .request_error( - ClientRequest::DeviceKeySign { - request_id: RequestId::Integer(20_004), - params: DeviceKeySignParams { - key_id: "dk_123".to_string(), - payload: DeviceKeySignPayload::RemoteControlClientConnection { - nonce: "nonce-123".to_string(), - audience: - RemoteControlClientConnectionAudience::RemoteControlClientWebsocket, - session_id: "wssess_123".to_string(), - target_origin: "https://chatgpt.com".to_string(), - target_path: "/api/codex/remote/control/client".to_string(), - account_user_id: "acct_123".to_string(), - client_id: "cli_123".to_string(), - token_expires_at: 4_102_444_800, - token_sha256_base64url: "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU" - .to_string(), - scopes: vec!["remote_control_controller_websocket".to_string()], - }, - }, - }, - /*trace*/ None, - ) - .await; - - assert_eq!(error.code, crate::error_code::INVALID_REQUEST_ERROR_CODE); - assert_eq!( - error.message, - "device/key/sign is not available over remote transports" - ); - - harness.shutdown().await; - Ok(()) -} - #[tokio::test(flavor = "current_thread")] #[serial(app_server_tracing)] async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> { diff --git a/codex-rs/app-server/src/request_processors.rs b/codex-rs/app-server/src/request_processors.rs index 52b15b0521b3..dc80494b6700 100644 --- a/codex-rs/app-server/src/request_processors.rs +++ b/codex-rs/app-server/src/request_processors.rs @@ -435,7 +435,6 @@ mod apps_processor; mod catalog_processor; mod command_exec_processor; mod config_processor; -mod device_key_processor; mod external_agent_config_processor; mod feedback_processor; mod fs_processor; @@ -456,7 +455,6 @@ pub(crate) use apps_processor::AppsRequestProcessor; pub(crate) use catalog_processor::CatalogRequestProcessor; pub(crate) use command_exec_processor::CommandExecRequestProcessor; pub(crate) use config_processor::ConfigRequestProcessor; -pub(crate) use device_key_processor::DeviceKeyRequestProcessor; pub(crate) use external_agent_config_processor::ExternalAgentConfigRequestProcessor; pub(crate) use feedback_processor::FeedbackRequestProcessor; pub(crate) use fs_processor::FsRequestProcessor; diff --git a/codex-rs/app-server/src/request_processors/device_key_processor.rs b/codex-rs/app-server/src/request_processors/device_key_processor.rs deleted file mode 100644 index ea0a96c2aff4..000000000000 --- a/codex-rs/app-server/src/request_processors/device_key_processor.rs +++ /dev/null @@ -1,369 +0,0 @@ -use std::fmt; -use std::future::Future; -use std::sync::Arc; - -use crate::error_code::internal_error; -use crate::error_code::invalid_request; -use crate::outgoing_message::ConnectionRequestId; -use crate::outgoing_message::OutgoingMessageSender; -use async_trait::async_trait; -use base64::Engine; -use base64::engine::general_purpose::STANDARD; -use codex_app_server_protocol::ClientResponsePayload; -use codex_app_server_protocol::DeviceKeyAlgorithm; -use codex_app_server_protocol::DeviceKeyCreateParams; -use codex_app_server_protocol::DeviceKeyCreateResponse; -use codex_app_server_protocol::DeviceKeyProtectionClass; -use codex_app_server_protocol::DeviceKeyPublicParams; -use codex_app_server_protocol::DeviceKeyPublicResponse; -use codex_app_server_protocol::DeviceKeySignParams; -use codex_app_server_protocol::DeviceKeySignPayload; -use codex_app_server_protocol::DeviceKeySignResponse; -use codex_app_server_protocol::JSONRPCErrorError; -use codex_device_key::DeviceKeyBinding; -use codex_device_key::DeviceKeyBindingStore; -use codex_device_key::DeviceKeyCreateRequest; -use codex_device_key::DeviceKeyError; -use codex_device_key::DeviceKeyGetPublicRequest; -use codex_device_key::DeviceKeyInfo; -use codex_device_key::DeviceKeyProtectionPolicy; -use codex_device_key::DeviceKeySignRequest; -use codex_device_key::DeviceKeyStore; -use codex_device_key::RemoteControlClientConnectionAudience; -use codex_device_key::RemoteControlClientConnectionSignPayload; -use codex_device_key::RemoteControlClientEnrollmentAudience; -use codex_device_key::RemoteControlClientEnrollmentSignPayload; -use codex_state::DeviceKeyBindingRecord; -use codex_state::StateRuntime; - -#[derive(Clone)] -pub(crate) struct DeviceKeyRequestProcessor { - outgoing: Arc, - store: DeviceKeyStore, -} - -impl DeviceKeyRequestProcessor { - pub(crate) fn new( - outgoing: Arc, - state_db: Option>, - ) -> Self { - Self { - outgoing, - store: DeviceKeyStore::new(Arc::new(StateDeviceKeyBindingStore::new(state_db))), - } - } - - pub(crate) fn create( - &self, - request_id: ConnectionRequestId, - params: DeviceKeyCreateParams, - device_key_requests_allowed: bool, - ) { - self.spawn_request( - request_id, - "device/key/create", - device_key_requests_allowed, - move |store| async move { create_device_key(store, params).await }, - ); - } - - pub(crate) fn public( - &self, - request_id: ConnectionRequestId, - params: DeviceKeyPublicParams, - device_key_requests_allowed: bool, - ) { - self.spawn_request( - request_id, - "device/key/public", - device_key_requests_allowed, - move |store| async move { public_device_key(store, params).await }, - ); - } - - pub(crate) fn sign( - &self, - request_id: ConnectionRequestId, - params: DeviceKeySignParams, - device_key_requests_allowed: bool, - ) { - self.spawn_request( - request_id, - "device/key/sign", - device_key_requests_allowed, - move |store| async move { sign_device_key(store, params).await }, - ); - } - - fn spawn_request( - &self, - request_id: ConnectionRequestId, - method: &'static str, - device_key_requests_allowed: bool, - run_request: F, - ) where - R: Into + Send + 'static, - F: FnOnce(DeviceKeyStore) -> Fut + Send + 'static, - Fut: Future> + Send + 'static, - { - let store = self.store.clone(); - let outgoing = Arc::clone(&self.outgoing); - tokio::spawn(async move { - let result = if !device_key_requests_allowed { - Err(invalid_request(format!( - "{method} is not available over remote transports" - ))) - } else { - run_request(store).await - }; - outgoing.send_result(request_id, result).await; - }); - } -} - -async fn create_device_key( - store: DeviceKeyStore, - params: DeviceKeyCreateParams, -) -> Result { - let info = store - .create(DeviceKeyCreateRequest { - protection_policy: protection_policy_from_params(params.protection_policy), - binding: DeviceKeyBinding { - account_user_id: params.account_user_id, - client_id: params.client_id, - }, - }) - .await - .map_err(map_device_key_error)?; - Ok(create_response_from_info(info)) -} - -async fn public_device_key( - store: DeviceKeyStore, - params: DeviceKeyPublicParams, -) -> Result { - let info = store - .get_public(DeviceKeyGetPublicRequest { - key_id: params.key_id, - }) - .await - .map_err(map_device_key_error)?; - Ok(public_response_from_info(info)) -} - -async fn sign_device_key( - store: DeviceKeyStore, - params: DeviceKeySignParams, -) -> Result { - let signature = store - .sign(DeviceKeySignRequest { - key_id: params.key_id, - payload: payload_from_params(params.payload), - }) - .await - .map_err(map_device_key_error)?; - Ok(DeviceKeySignResponse { - signature_der_base64: STANDARD.encode(signature.signature_der), - signed_payload_base64: STANDARD.encode(signature.signed_payload), - algorithm: algorithm_from_store(signature.algorithm), - }) -} - -struct StateDeviceKeyBindingStore { - state_db: Option>, -} - -impl StateDeviceKeyBindingStore { - fn new(state_db: Option>) -> Self { - Self { state_db } - } - - async fn state_db(&self) -> Result, DeviceKeyError> { - self.state_db - .clone() - .ok_or_else(|| DeviceKeyError::Platform("sqlite state db unavailable".to_string())) - } -} - -impl fmt::Debug for StateDeviceKeyBindingStore { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("StateDeviceKeyBindingStore") - .field("has_state_db", &self.state_db.is_some()) - .finish_non_exhaustive() - } -} - -#[async_trait] -impl DeviceKeyBindingStore for StateDeviceKeyBindingStore { - async fn get_binding(&self, key_id: &str) -> Result, DeviceKeyError> { - let state_db = self.state_db().await?; - state_db - .get_device_key_binding(key_id) - .await - .map(|record| { - record.map(|record| DeviceKeyBinding { - account_user_id: record.account_user_id, - client_id: record.client_id, - }) - }) - .map_err(|err| DeviceKeyError::Platform(err.to_string())) - } - - async fn put_binding( - &self, - key_id: &str, - binding: &DeviceKeyBinding, - ) -> Result<(), DeviceKeyError> { - let state_db = self.state_db().await?; - state_db - .upsert_device_key_binding(&DeviceKeyBindingRecord { - key_id: key_id.to_string(), - account_user_id: binding.account_user_id.clone(), - client_id: binding.client_id.clone(), - }) - .await - .map_err(|err| DeviceKeyError::Platform(err.to_string())) - } -} - -fn create_response_from_info(info: DeviceKeyInfo) -> DeviceKeyCreateResponse { - DeviceKeyCreateResponse { - key_id: info.key_id, - public_key_spki_der_base64: STANDARD.encode(info.public_key_spki_der), - algorithm: algorithm_from_store(info.algorithm), - protection_class: protection_class_from_store(info.protection_class), - } -} - -fn public_response_from_info(info: DeviceKeyInfo) -> DeviceKeyPublicResponse { - DeviceKeyPublicResponse { - key_id: info.key_id, - public_key_spki_der_base64: STANDARD.encode(info.public_key_spki_der), - algorithm: algorithm_from_store(info.algorithm), - protection_class: protection_class_from_store(info.protection_class), - } -} - -fn protection_policy_from_params( - protection_policy: Option, -) -> DeviceKeyProtectionPolicy { - match protection_policy - .unwrap_or(codex_app_server_protocol::DeviceKeyProtectionPolicy::HardwareOnly) - { - codex_app_server_protocol::DeviceKeyProtectionPolicy::HardwareOnly => { - DeviceKeyProtectionPolicy::HardwareOnly - } - codex_app_server_protocol::DeviceKeyProtectionPolicy::AllowOsProtectedNonextractable => { - DeviceKeyProtectionPolicy::AllowOsProtectedNonextractable - } - } -} - -fn payload_from_params(payload: DeviceKeySignPayload) -> codex_device_key::DeviceKeySignPayload { - match payload { - DeviceKeySignPayload::RemoteControlClientConnection { - nonce, - audience, - session_id, - target_origin, - target_path, - account_user_id, - client_id, - token_sha256_base64url, - token_expires_at, - scopes, - } => codex_device_key::DeviceKeySignPayload::RemoteControlClientConnection( - RemoteControlClientConnectionSignPayload { - nonce, - audience: remote_control_client_connection_audience_from_protocol(audience), - session_id, - target_origin, - target_path, - account_user_id, - client_id, - token_sha256_base64url, - token_expires_at, - scopes, - }, - ), - DeviceKeySignPayload::RemoteControlClientEnrollment { - nonce, - audience, - challenge_id, - target_origin, - target_path, - account_user_id, - client_id, - device_identity_sha256_base64url, - challenge_expires_at, - } => codex_device_key::DeviceKeySignPayload::RemoteControlClientEnrollment( - RemoteControlClientEnrollmentSignPayload { - nonce, - audience: remote_control_client_enrollment_audience_from_protocol(audience), - challenge_id, - target_origin, - target_path, - account_user_id, - client_id, - device_identity_sha256_base64url, - challenge_expires_at, - }, - ), - } -} - -fn remote_control_client_connection_audience_from_protocol( - audience: codex_app_server_protocol::RemoteControlClientConnectionAudience, -) -> RemoteControlClientConnectionAudience { - match audience { - codex_app_server_protocol::RemoteControlClientConnectionAudience::RemoteControlClientWebsocket => { - RemoteControlClientConnectionAudience::RemoteControlClientWebsocket - } - } -} - -fn remote_control_client_enrollment_audience_from_protocol( - audience: codex_app_server_protocol::RemoteControlClientEnrollmentAudience, -) -> RemoteControlClientEnrollmentAudience { - match audience { - codex_app_server_protocol::RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment => { - RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment - } - } -} - -fn algorithm_from_store(algorithm: codex_device_key::DeviceKeyAlgorithm) -> DeviceKeyAlgorithm { - match algorithm { - codex_device_key::DeviceKeyAlgorithm::EcdsaP256Sha256 => { - DeviceKeyAlgorithm::EcdsaP256Sha256 - } - } -} - -fn protection_class_from_store( - protection_class: codex_device_key::DeviceKeyProtectionClass, -) -> DeviceKeyProtectionClass { - match protection_class { - codex_device_key::DeviceKeyProtectionClass::HardwareSecureEnclave => { - DeviceKeyProtectionClass::HardwareSecureEnclave - } - codex_device_key::DeviceKeyProtectionClass::HardwareTpm => { - DeviceKeyProtectionClass::HardwareTpm - } - codex_device_key::DeviceKeyProtectionClass::OsProtectedNonextractable => { - DeviceKeyProtectionClass::OsProtectedNonextractable - } - } -} - -fn map_device_key_error(error: DeviceKeyError) -> JSONRPCErrorError { - match &error { - DeviceKeyError::DegradedProtectionNotAllowed { .. } - | DeviceKeyError::HardwareBackedKeysUnavailable - | DeviceKeyError::KeyNotFound - | DeviceKeyError::InvalidPayload(_) => invalid_request(error.to_string()), - DeviceKeyError::Platform(_) | DeviceKeyError::Crypto(_) => { - internal_error(error.to_string()) - } - } -} diff --git a/codex-rs/app-server/src/transport.rs b/codex-rs/app-server/src/transport.rs index 9c16f8a3941f..8d61ac5f56d3 100644 --- a/codex-rs/app-server/src/transport.rs +++ b/codex-rs/app-server/src/transport.rs @@ -36,7 +36,7 @@ pub(crate) struct ConnectionState { impl ConnectionState { pub(crate) fn new( - origin: ConnectionOrigin, + _origin: ConnectionOrigin, outbound_initialized: Arc, outbound_experimental_api_enabled: Arc, outbound_opted_out_notification_methods: Arc>>, @@ -45,7 +45,7 @@ impl ConnectionState { outbound_initialized, outbound_experimental_api_enabled, outbound_opted_out_notification_methods, - session: Arc::new(ConnectionSessionState::new(origin)), + session: Arc::new(ConnectionSessionState::new()), } } } diff --git a/codex-rs/app-server/tests/suite/v2/device_key.rs b/codex-rs/app-server/tests/suite/v2/device_key.rs deleted file mode 100644 index f8a4d0cf67b3..000000000000 --- a/codex-rs/app-server/tests/suite/v2/device_key.rs +++ /dev/null @@ -1,119 +0,0 @@ -use super::connection_handling_websocket::connect_websocket; -use super::connection_handling_websocket::create_config_toml; -use super::connection_handling_websocket::read_error_for_id; -use super::connection_handling_websocket::read_response_for_id; -use super::connection_handling_websocket::send_initialize_request; -use super::connection_handling_websocket::send_request; -use super::connection_handling_websocket::spawn_websocket_server; -use anyhow::Result; -use app_test_support::McpProcess; -use app_test_support::create_mock_responses_server_sequence_unchecked; -use codex_app_server_protocol::RequestId; -use pretty_assertions::assert_eq; -use serde_json::json; -use tempfile::TempDir; -use tokio::time::Duration; -use tokio::time::timeout; - -#[cfg(any(target_os = "macos", windows))] -const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60); -#[cfg(not(any(target_os = "macos", windows)))] -const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10); - -async fn initialized_mcp(codex_home: &TempDir) -> Result { - let mut mcp = McpProcess::new(codex_home.path()).await?; - timeout(DEFAULT_READ_TIMEOUT, mcp.initialize()).await??; - Ok(mcp) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn device_key_create_rejects_empty_account_user_id() -> Result<()> { - let codex_home = TempDir::new()?; - let mut mcp = initialized_mcp(&codex_home).await?; - - let request_id = mcp - .send_raw_request( - "device/key/create", - Some(json!({ - "accountUserId": "", - "clientId": "cli_123", - })), - ) - .await?; - let error = timeout( - DEFAULT_READ_TIMEOUT, - mcp.read_stream_until_error_message(RequestId::Integer(request_id)), - ) - .await??; - - assert_eq!(error.error.code, -32600); - assert_eq!( - error.error.message, - "invalid device key payload: accountUserId must not be empty" - ); - - Ok(()) -} - -#[tokio::test(flavor = "multi_thread", worker_threads = 2)] -async fn device_key_methods_are_rejected_over_websocket() -> Result<()> { - let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await; - let codex_home = TempDir::new()?; - create_config_toml(codex_home.path(), &server.uri(), "never")?; - - let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?; - let mut ws = connect_websocket(bind_addr).await?; - send_initialize_request(&mut ws, /*id*/ 1, "device_key_ws_test").await?; - let initialize_response = read_response_for_id(&mut ws, /*id*/ 1).await?; - assert_eq!(initialize_response.id, RequestId::Integer(1)); - - let cases = [ - ( - "device/key/create", - json!({ - "accountUserId": "acct_123", - "clientId": "cli_123", - }), - ), - ( - "device/key/public", - json!({ - "keyId": "device-key-123", - }), - ), - ( - "device/key/sign", - json!({ - "keyId": "device-key-123", - "payload": { - "type": "remoteControlClientConnection", - "nonce": "nonce-123", - "audience": "remote_control_client_websocket", - "sessionId": "wssess_123", - "targetOrigin": "https://chatgpt.com", - "targetPath": "/api/codex/remote/control/client", - "accountUserId": "acct_123", - "clientId": "cli_123", - "tokenExpiresAt": 4_102_444_800i64, - "tokenSha256Base64url": "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU", - "scopes": ["remote_control_controller_websocket"], - }, - }), - ), - ]; - - for (index, (method, params)) in cases.into_iter().enumerate() { - let id = 2 + index as i64; - send_request(&mut ws, method, id, Some(params)).await?; - let error = read_error_for_id(&mut ws, id).await?; - - assert_eq!(error.error.code, -32600); - assert_eq!( - error.error.message, - format!("{method} is not available over remote transports") - ); - } - - process.kill().await?; - Ok(()) -} diff --git a/codex-rs/app-server/tests/suite/v2/mod.rs b/codex-rs/app-server/tests/suite/v2/mod.rs index be5f12a535ed..8e13df7825f4 100644 --- a/codex-rs/app-server/tests/suite/v2/mod.rs +++ b/codex-rs/app-server/tests/suite/v2/mod.rs @@ -10,7 +10,6 @@ mod config_rpc; mod connection_handling_websocket; #[cfg(unix)] mod connection_handling_websocket_unix; -mod device_key; mod dynamic_tools; mod experimental_api; mod experimental_feature_list; diff --git a/codex-rs/device-key/BUILD.bazel b/codex-rs/device-key/BUILD.bazel deleted file mode 100644 index 4ad47f84a0d2..000000000000 --- a/codex-rs/device-key/BUILD.bazel +++ /dev/null @@ -1,6 +0,0 @@ -load("//:defs.bzl", "codex_rust_crate") - -codex_rust_crate( - name = "device-key", - crate_name = "codex_device_key", -) diff --git a/codex-rs/device-key/Cargo.toml b/codex-rs/device-key/Cargo.toml deleted file mode 100644 index 6ad280efc85f..000000000000 --- a/codex-rs/device-key/Cargo.toml +++ /dev/null @@ -1,22 +0,0 @@ -[package] -name = "codex-device-key" -version.workspace = true -edition.workspace = true -license.workspace = true - -[lints] -workspace = true - -[dependencies] -async-trait = { workspace = true } -base64 = { workspace = true } -p256 = { workspace = true, features = ["ecdsa", "pkcs8"] } -rand = { workspace = true } -serde = { workspace = true, features = ["derive"] } -serde_json = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["rt"] } -url = { workspace = true } - -[dev-dependencies] -pretty_assertions = { workspace = true } diff --git a/codex-rs/device-key/src/lib.rs b/codex-rs/device-key/src/lib.rs deleted file mode 100644 index f901c633c99c..000000000000 --- a/codex-rs/device-key/src/lib.rs +++ /dev/null @@ -1,1495 +0,0 @@ -use async_trait::async_trait; -use base64::Engine; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use p256::pkcs8::EncodePublicKey; -use rand::random; -use serde::Deserialize; -use serde::Serialize; -use std::fmt; -use std::fmt::Debug; -use std::sync::Arc; -use std::time::SystemTime; -use std::time::UNIX_EPOCH; -use thiserror::Error; -use url::Host; -use url::Url; - -mod platform; - -const SIGNING_DOMAIN: &str = "codex-device-key-sign-payload/v1"; -const DEVICE_KEY_ID_RANDOM_BYTES: usize = 32; -const DEVICE_KEY_ID_ENCODED_BYTES: usize = 43; -const DEVICE_KEY_ID_HARDWARE_SECURE_ENCLAVE_PREFIX: &str = "dk_hse_"; -const DEVICE_KEY_ID_HARDWARE_TPM_PREFIX: &str = "dk_tpm_"; -const DEVICE_KEY_ID_OS_PROTECTED_NONEXTRACTABLE_PREFIX: &str = "dk_osn_"; -const DEVICE_KEY_ID_PREFIX_LEN: usize = DEVICE_KEY_ID_HARDWARE_SECURE_ENCLAVE_PREFIX.len(); -const DEVICE_KEY_ID_LEN: usize = DEVICE_KEY_ID_PREFIX_LEN + DEVICE_KEY_ID_ENCODED_BYTES; -const INVALID_DEVICE_KEY_ID_MESSAGE: &str = - "keyId must be dk_hse_, dk_tpm_, or dk_osn_ followed by unpadded base64url-encoded 32 bytes"; -const REMOTE_CONTROL_CONTROLLER_WEBSOCKET_SCOPE: &str = "remote_control_controller_websocket"; -const MAX_REMOTE_CONTROL_DEVICE_KEY_PROOF_TTL_SECONDS: i64 = 15 * 60; -const REMOTE_CONTROL_CLIENT_CONNECTION_PATHS: &[&str] = &[ - "/api/codex/remote/control/client", - "/wham/remote/control/client", -]; -const REMOTE_CONTROL_CLIENT_ENROLLMENT_PATHS: &[&str] = &[ - "/api/codex/remote/control/client/enroll", - "/wham/remote/control/client/enroll", -]; - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum DeviceKeyAlgorithm { - EcdsaP256Sha256, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum DeviceKeyProtectionClass { - HardwareSecureEnclave, - HardwareTpm, - OsProtectedNonextractable, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum DeviceKeyProtectionPolicy { - HardwareOnly, - AllowOsProtectedNonextractable, -} - -impl DeviceKeyProtectionPolicy { - fn allows(self, protection_class: DeviceKeyProtectionClass) -> bool { - match self { - Self::HardwareOnly => !protection_class.is_degraded(), - Self::AllowOsProtectedNonextractable => matches!( - protection_class, - DeviceKeyProtectionClass::HardwareSecureEnclave - | DeviceKeyProtectionClass::HardwareTpm - | DeviceKeyProtectionClass::OsProtectedNonextractable - ), - } - } -} - -impl DeviceKeyProtectionClass { - pub fn is_degraded(self) -> bool { - match self { - Self::HardwareSecureEnclave | Self::HardwareTpm => false, - Self::OsProtectedNonextractable => true, - } - } - - fn key_id_prefix(self) -> &'static str { - match self { - Self::HardwareSecureEnclave => DEVICE_KEY_ID_HARDWARE_SECURE_ENCLAVE_PREFIX, - Self::HardwareTpm => DEVICE_KEY_ID_HARDWARE_TPM_PREFIX, - Self::OsProtectedNonextractable => DEVICE_KEY_ID_OS_PROTECTED_NONEXTRACTABLE_PREFIX, - } - } -} - -impl fmt::Display for DeviceKeyProtectionClass { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::HardwareSecureEnclave => f.write_str("hardware_secure_enclave"), - Self::HardwareTpm => f.write_str("hardware_tpm"), - Self::OsProtectedNonextractable => f.write_str("os_protected_nonextractable"), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DeviceKeyCreateRequest { - pub protection_policy: DeviceKeyProtectionPolicy, - pub binding: DeviceKeyBinding, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DeviceKeyGetPublicRequest { - pub key_id: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DeviceKeySignRequest { - pub key_id: String, - pub payload: DeviceKeySignPayload, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DeviceKeyBinding { - pub account_user_id: String, - pub client_id: String, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DeviceKeyInfo { - pub key_id: String, - pub public_key_spki_der: Vec, - pub algorithm: DeviceKeyAlgorithm, - pub protection_class: DeviceKeyProtectionClass, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DeviceKeySignature { - pub signature_der: Vec, - /// Exact payload bytes covered by `signature_der`. - pub signed_payload: Vec, - pub algorithm: DeviceKeyAlgorithm, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct ProviderSignature { - signature_der: Vec, - algorithm: DeviceKeyAlgorithm, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "camelCase")] -pub enum DeviceKeySignPayload { - RemoteControlClientConnection(RemoteControlClientConnectionSignPayload), - RemoteControlClientEnrollment(RemoteControlClientEnrollmentSignPayload), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RemoteControlClientConnectionAudience { - RemoteControlClientWebsocket, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RemoteControlClientConnectionSignPayload { - pub nonce: String, - pub audience: RemoteControlClientConnectionAudience, - pub session_id: String, - pub target_origin: String, - pub target_path: String, - pub account_user_id: String, - pub client_id: String, - pub token_sha256_base64url: String, - pub token_expires_at: i64, - pub scopes: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum RemoteControlClientEnrollmentAudience { - RemoteControlClientEnrollment, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct RemoteControlClientEnrollmentSignPayload { - pub nonce: String, - pub audience: RemoteControlClientEnrollmentAudience, - pub challenge_id: String, - pub target_origin: String, - pub target_path: String, - pub account_user_id: String, - pub client_id: String, - pub device_identity_sha256_base64url: String, - pub challenge_expires_at: i64, -} - -#[derive(Debug, Error)] -pub enum DeviceKeyError { - #[error( - "hardware-backed device keys are not available; set protectionPolicy to allow_os_protected_nonextractable to allow key protection class {available}" - )] - DegradedProtectionNotAllowed { available: DeviceKeyProtectionClass }, - #[error("hardware-backed device keys are not available on this platform")] - HardwareBackedKeysUnavailable, - #[error("device key not found")] - KeyNotFound, - #[error("invalid device key payload: {0}")] - InvalidPayload(&'static str), - #[error("device key platform error: {0}")] - Platform(String), - #[error("device key cryptography error: {0}")] - Crypto(String), -} - -#[derive(Debug, Clone)] -pub struct DeviceKeyStore { - provider: Arc, - bindings: Arc, -} - -impl DeviceKeyStore { - pub fn new(bindings: Arc) -> Self { - Self { - provider: platform::default_provider(), - bindings, - } - } - - pub async fn create( - &self, - request: DeviceKeyCreateRequest, - ) -> Result { - let key_id_random = random_key_id_random(); - validate_binding(&request.binding.account_user_id, &request.binding.client_id)?; - let provider = Arc::clone(&self.provider); - let info = spawn_provider_call(move || { - provider.create(ProviderCreateRequest { - key_id_random, - protection_policy: request.protection_policy, - }) - }) - .await?; - match self - .bindings - .put_binding(&info.key_id, &request.binding) - .await - { - Ok(()) => Ok(info), - Err(store_error) => { - let provider = Arc::clone(&self.provider); - let key_id = info.key_id; - let protection_class = info.protection_class; - if let Err(delete_error) = - spawn_provider_call(move || provider.delete(&key_id, protection_class)).await - { - return Err(DeviceKeyError::Platform(format!( - "failed to store device key binding ({store_error}); failed to delete newly created key ({delete_error})" - ))); - } - Err(store_error) - } - } - } - - pub async fn get_public( - &self, - request: DeviceKeyGetPublicRequest, - ) -> Result { - let protection_class = validate_key_id(&request.key_id)?; - let provider = Arc::clone(&self.provider); - spawn_provider_call(move || provider.get_public(&request.key_id, protection_class)).await - } - - pub async fn sign( - &self, - request: DeviceKeySignRequest, - ) -> Result { - let protection_class = validate_key_id(&request.key_id)?; - validate_payload(&request.payload)?; - let binding = self - .bindings - .get_binding(&request.key_id) - .await? - .ok_or(DeviceKeyError::KeyNotFound)?; - validate_payload_binding(&request.payload, &binding)?; - let signed_payload = device_key_signing_payload_bytes(&request.payload)?; - let provider = Arc::clone(&self.provider); - let key_id = request.key_id; - let provider_payload = signed_payload.clone(); - let signature = spawn_provider_call(move || { - provider.sign(&key_id, protection_class, &provider_payload) - }) - .await?; - Ok(DeviceKeySignature { - signature_der: signature.signature_der, - signed_payload, - algorithm: signature.algorithm, - }) - } - - #[cfg(test)] - fn new_for_test(provider: Arc) -> Self { - Self { - provider, - bindings: Arc::new(InMemoryDeviceKeyBindingStore::default()), - } - } -} - -async fn spawn_provider_call(call: F) -> Result -where - T: Send + 'static, - F: FnOnce() -> Result + Send + 'static, -{ - tokio::task::spawn_blocking(call) - .await - .map_err(|err| DeviceKeyError::Platform(format!("device key task failed: {err}")))? -} - -/// Persists the account/client binding for a generated device key. -/// -/// Device-key providers only own platform key material. Implementations store the binding in a -/// platform-neutral location so signing can reject payloads for the wrong account or client before -/// asking a provider to use the private key. -#[async_trait] -pub trait DeviceKeyBindingStore: Debug + Send + Sync { - async fn get_binding(&self, key_id: &str) -> Result, DeviceKeyError>; - async fn put_binding( - &self, - key_id: &str, - binding: &DeviceKeyBinding, - ) -> Result<(), DeviceKeyError>; -} - -#[cfg(test)] -#[derive(Debug, Default)] -struct InMemoryDeviceKeyBindingStore { - bindings: std::sync::Mutex>, -} - -#[cfg(test)] -#[async_trait] -impl DeviceKeyBindingStore for InMemoryDeviceKeyBindingStore { - async fn get_binding(&self, key_id: &str) -> Result, DeviceKeyError> { - Ok(self - .bindings - .lock() - .map_err(|err| DeviceKeyError::Platform(err.to_string()))? - .get(key_id) - .cloned()) - } - - async fn put_binding( - &self, - key_id: &str, - binding: &DeviceKeyBinding, - ) -> Result<(), DeviceKeyError> { - self.bindings - .lock() - .map_err(|err| DeviceKeyError::Platform(err.to_string()))? - .insert(key_id.to_string(), binding.clone()); - Ok(()) - } -} - -#[derive(Debug)] -struct ProviderCreateRequest { - key_id_random: String, - protection_policy: DeviceKeyProtectionPolicy, -} - -impl ProviderCreateRequest { - fn key_id_for(&self, protection_class: DeviceKeyProtectionClass) -> String { - key_id_for_protection_class(protection_class, &self.key_id_random) - } -} - -/// Owns platform-specific non-exportable key operations for device signing. -/// -/// Implementations must never expose a generic arbitrary-byte signing API outside this crate. The -/// crate validates and serializes accepted structured payloads before calling `sign`. -trait DeviceKeyProvider: Debug + Send + Sync { - fn create(&self, request: ProviderCreateRequest) -> Result; - /// Deletes provider-owned key material after a create operation cannot be completed. - /// - /// Implementations should treat missing keys as success where the platform allows it, since - /// cleanup can race with external deletion and should not mask the original persistence error - /// unless deletion itself fails unexpectedly. - fn delete( - &self, - key_id: &str, - protection_class: DeviceKeyProtectionClass, - ) -> Result<(), DeviceKeyError>; - fn get_public( - &self, - key_id: &str, - protection_class: DeviceKeyProtectionClass, - ) -> Result; - fn sign( - &self, - key_id: &str, - protection_class: DeviceKeyProtectionClass, - payload: &[u8], - ) -> Result; -} - -fn random_key_id_random() -> String { - URL_SAFE_NO_PAD.encode(random::<[u8; DEVICE_KEY_ID_RANDOM_BYTES]>()) -} - -fn key_id_for_protection_class( - protection_class: DeviceKeyProtectionClass, - encoded_random: &str, -) -> String { - format!("{}{encoded_random}", protection_class.key_id_prefix()) -} - -/// Validates the account/client binding stored with a key or embedded in an accepted payload. -/// -/// Providers treat the binding as metadata, so this crate keeps empty values from entering the -/// store and later matching every other empty value by accident. -fn validate_binding(account_user_id: &str, client_id: &str) -> Result<(), DeviceKeyError> { - if account_user_id.is_empty() { - return Err(DeviceKeyError::InvalidPayload( - "accountUserId must not be empty", - )); - } - if client_id.is_empty() { - return Err(DeviceKeyError::InvalidPayload("clientId must not be empty")); - } - Ok(()) -} - -/// Keeps all externally supplied key IDs inside the random `dk_*_` namespaces created by this crate. -/// -/// Platform providers use the key ID in OS-specific labels, tags, and metadata paths. Requiring the -/// exact generated shape avoids path or tag surprises and makes the namespace auditable. -fn validate_key_id(key_id: &str) -> Result { - let (protection_class, encoded_key) = parse_key_id(key_id).ok_or( - DeviceKeyError::InvalidPayload(INVALID_DEVICE_KEY_ID_MESSAGE), - )?; - if key_id.len() != DEVICE_KEY_ID_LEN { - return Err(DeviceKeyError::InvalidPayload( - INVALID_DEVICE_KEY_ID_MESSAGE, - )); - } - if !URL_SAFE_NO_PAD - .decode(encoded_key) - .is_ok_and(|decoded| decoded.len() == DEVICE_KEY_ID_RANDOM_BYTES) - { - return Err(DeviceKeyError::InvalidPayload( - INVALID_DEVICE_KEY_ID_MESSAGE, - )); - } - Ok(protection_class) -} - -fn parse_key_id(key_id: &str) -> Option<(DeviceKeyProtectionClass, &str)> { - for protection_class in [ - DeviceKeyProtectionClass::HardwareSecureEnclave, - DeviceKeyProtectionClass::HardwareTpm, - DeviceKeyProtectionClass::OsProtectedNonextractable, - ] { - if let Some(encoded_key) = key_id.strip_prefix(protection_class.key_id_prefix()) { - return Some((protection_class, encoded_key)); - } - } - None -} - -/// Confirms the signed payload is for the same account/client binding as the selected device key. -/// -/// The provider can prove continuity of the key material, but app-server authorization depends on -/// binding that key to the same account and client identity used by the remote-control flow. -fn validate_payload_binding( - payload: &DeviceKeySignPayload, - binding: &DeviceKeyBinding, -) -> Result<(), DeviceKeyError> { - let (account_user_id, client_id) = match payload { - DeviceKeySignPayload::RemoteControlClientConnection(payload) => { - (&payload.account_user_id, &payload.client_id) - } - DeviceKeySignPayload::RemoteControlClientEnrollment(payload) => { - (&payload.account_user_id, &payload.client_id) - } - }; - if account_user_id != &binding.account_user_id || client_id != &binding.client_id { - return Err(DeviceKeyError::InvalidPayload( - "payload accountUserId/clientId does not match device key binding", - )); - } - Ok(()) -} - -/// Dispatches validation by accepted payload shape before any provider sees bytes to sign. -/// -/// The enum is intentionally narrow so adding another signing use case requires defining and -/// validating a new structured payload variant here. -fn validate_payload(payload: &DeviceKeySignPayload) -> Result<(), DeviceKeyError> { - match payload { - DeviceKeySignPayload::RemoteControlClientConnection(payload) => { - validate_remote_control_client_connection_payload(payload) - } - DeviceKeySignPayload::RemoteControlClientEnrollment(payload) => { - validate_remote_control_client_enrollment_payload(payload) - } - } -} - -/// Validates payloads used to prove device-key ownership while opening `/client`. -/// -/// This shape is scoped to a single controller websocket connection and is only allowed to target -/// the non-enrollment remote-control client endpoints. -fn validate_remote_control_client_connection_payload( - payload: &RemoteControlClientConnectionSignPayload, -) -> Result<(), DeviceKeyError> { - validate_nonce(&payload.nonce)?; - validate_remote_control_target( - &payload.target_origin, - &payload.target_path, - REMOTE_CONTROL_CLIENT_CONNECTION_PATHS, - )?; - if payload.session_id.is_empty() { - return Err(DeviceKeyError::InvalidPayload( - "sessionId must not be empty", - )); - } - validate_binding(&payload.account_user_id, &payload.client_id)?; - if !is_base64url_sha256(&payload.token_sha256_base64url) { - return Err(DeviceKeyError::InvalidPayload( - "tokenSha256Base64url must be a SHA-256 digest encoded as unpadded base64url", - )); - } - if payload.scopes != [REMOTE_CONTROL_CONTROLLER_WEBSOCKET_SCOPE] { - return Err(DeviceKeyError::InvalidPayload( - "scopes must contain exactly remote_control_controller_websocket", - )); - } - validate_remote_control_expiry(payload.token_expires_at, "remote-control token")?; - Ok(()) -} - -/// Validates payloads used during device-key enrollment. -/// -/// Enrollment has a distinct payload shape and challenge identifier, so it also carries a distinct -/// endpoint allowlist from connection proofs. -fn validate_remote_control_client_enrollment_payload( - payload: &RemoteControlClientEnrollmentSignPayload, -) -> Result<(), DeviceKeyError> { - validate_nonce(&payload.nonce)?; - if payload.challenge_id.is_empty() { - return Err(DeviceKeyError::InvalidPayload( - "challengeId must not be empty", - )); - } - validate_remote_control_target( - &payload.target_origin, - &payload.target_path, - REMOTE_CONTROL_CLIENT_ENROLLMENT_PATHS, - )?; - validate_binding(&payload.account_user_id, &payload.client_id)?; - if !is_base64url_sha256(&payload.device_identity_sha256_base64url) { - return Err(DeviceKeyError::InvalidPayload( - "deviceIdentitySha256Base64url must be a SHA-256 digest encoded as unpadded base64url", - )); - } - validate_remote_control_expiry(payload.challenge_expires_at, "enrollment challenge")?; - Ok(()) -} - -/// Requires a fresh server-issued challenge with enough entropy to prevent replay guessing. -fn validate_nonce(nonce: &str) -> Result<(), DeviceKeyError> { - if !URL_SAFE_NO_PAD - .decode(nonce) - .is_ok_and(|decoded| decoded.len() >= 32) - { - return Err(DeviceKeyError::InvalidPayload( - "nonce must be at least 32 random bytes encoded as unpadded base64url", - )); - } - Ok(()) -} - -/// Validates the remote backend origin and the endpoint set for the specific signed payload shape. -/// -/// Keeping the path allowlist as an argument makes it hard to accidentally let enrollment payloads -/// sign connection endpoints, or connection payloads sign enrollment endpoints. -fn validate_remote_control_target( - target_origin: &str, - target_path: &str, - allowed_target_paths: &[&str], -) -> Result<(), DeviceKeyError> { - if !is_allowed_remote_control_origin(target_origin) { - return Err(DeviceKeyError::InvalidPayload( - "targetOrigin must be an allowed remote-control backend origin", - )); - } - if !allowed_target_paths.contains(&target_path) { - return Err(DeviceKeyError::InvalidPayload( - "targetPath must match the signed payload type's remote-control endpoint", - )); - } - Ok(()) -} - -/// Mirrors the remote-control transport allowlist for origins that may receive signed proofs. -fn is_allowed_remote_control_origin(target_origin: &str) -> bool { - let Ok(url) = Url::parse(target_origin) else { - return false; - }; - if url.path() != "/" || url.query().is_some() || url.fragment().is_some() { - return false; - } - let host = url.host(); - match url.scheme() { - "https" if is_localhost(&host) || is_allowed_chatgpt_host(&host) => true, - "http" if is_localhost(&host) => true, - _ => false, - } -} - -/// Accepts first-party chatgpt.com hosts and staging equivalents, including subdomains. -fn is_allowed_chatgpt_host(host: &Option>) -> bool { - let Some(Host::Domain(host)) = *host else { - return false; - }; - host == "chatgpt.com" - || host == "chatgpt-staging.com" - || host.ends_with(".chatgpt.com") - || host.ends_with(".chatgpt-staging.com") -} - -/// Allows local development endpoints without opening access to arbitrary private-network hosts. -fn is_localhost(host: &Option>) -> bool { - match host { - Some(Host::Domain("localhost")) => true, - Some(Host::Ipv4(ip)) => ip.is_loopback(), - Some(Host::Ipv6(ip)) => ip.is_loopback(), - _ => false, - } -} - -/// Bounds remote-control proofs to the connection or enrollment attempt that requested them. -fn validate_remote_control_expiry( - expires_at: i64, - label: &'static str, -) -> Result<(), DeviceKeyError> { - let now = current_unix_seconds()?; - if expires_at <= now { - return Err(DeviceKeyError::InvalidPayload(match label { - "enrollment challenge" => "enrollment challenge is expired", - _ => "remote-control token is expired", - })); - } - if expires_at > now + MAX_REMOTE_CONTROL_DEVICE_KEY_PROOF_TTL_SECONDS { - return Err(DeviceKeyError::InvalidPayload(match label { - "enrollment challenge" => "enrollment challenge expires too far in the future", - _ => "remote-control token expires too far in the future", - })); - } - Ok(()) -} - -/// Checks the exact digest encoding used in remote-control challenge and token bindings. -fn is_base64url_sha256(value: &str) -> bool { - URL_SAFE_NO_PAD - .decode(value) - .is_ok_and(|digest| digest.len() == 32) -} - -fn current_unix_seconds() -> Result { - let duration = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map_err(|_| DeviceKeyError::InvalidPayload("system clock is before Unix epoch"))?; - i64::try_from(duration.as_secs()) - .map_err(|_| DeviceKeyError::InvalidPayload("current time does not fit in i64")) -} - -/// Returns the exact bytes that device-key providers sign and verifiers must check. -/// -/// The representation is UTF-8 JSON with an explicit domain separator, sorted object keys, no -/// insignificant whitespace, and the accepted structured payload. Test vectors in this crate -/// intentionally lock the field names and ordering so non-Rust verifiers can reproduce the same -/// bytes. -pub fn device_key_signing_payload_bytes( - payload: &DeviceKeySignPayload, -) -> Result, DeviceKeyError> { - let mut canonical = serde_json::to_value(SignedPayload { - domain: SIGNING_DOMAIN, - payload, - }) - .map_err(|err| DeviceKeyError::Crypto(err.to_string()))?; - canonical.sort_all_objects(); - serde_json::to_vec(&canonical).map_err(|err| DeviceKeyError::Crypto(err.to_string())) -} - -#[derive(Serialize)] -struct SignedPayload<'a> { - domain: &'static str, - payload: &'a DeviceKeySignPayload, -} - -#[allow(dead_code)] -fn sec1_public_key_to_spki_der(sec1_public_key: &[u8]) -> Result, DeviceKeyError> { - let public_key = p256::PublicKey::from_sec1_bytes(sec1_public_key) - .map_err(|err| DeviceKeyError::Crypto(err.to_string()))?; - public_key - .to_public_key_der() - .map(|der| der.as_bytes().to_vec()) - .map_err(|err| DeviceKeyError::Crypto(err.to_string())) -} - -#[cfg(test)] -mod tests { - use super::*; - use p256::ecdsa::Signature; - use p256::ecdsa::SigningKey; - use p256::ecdsa::VerifyingKey; - use p256::ecdsa::signature::Signer; - use p256::ecdsa::signature::Verifier; - use p256::elliptic_curve::rand_core::OsRng; - use p256::pkcs8::DecodePublicKey; - use pretty_assertions::assert_eq; - use std::collections::HashMap; - use std::sync::Mutex; - - const TEST_TOKEN_SHA256_BASE64URL: &str = "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU"; - const TEST_NONCE_BASE64URL: &str = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; - - #[derive(Debug)] - struct MemoryProvider { - class: DeviceKeyProtectionClass, - keys: Mutex>, - } - - impl MemoryProvider { - fn new(class: DeviceKeyProtectionClass) -> Self { - Self { - class, - keys: Mutex::new(HashMap::new()), - } - } - - fn key_count(&self) -> usize { - self.keys.lock().expect("memory provider lock").len() - } - } - - impl DeviceKeyProvider for MemoryProvider { - fn create(&self, request: ProviderCreateRequest) -> Result { - if !request.protection_policy.allows(self.class) { - return Err(DeviceKeyError::DegradedProtectionNotAllowed { - available: self.class, - }); - } - let key_id = request.key_id_for(self.class); - let mut keys = self - .keys - .lock() - .map_err(|err| DeviceKeyError::Platform(err.to_string()))?; - let signing_key = keys - .entry(key_id.clone()) - .or_insert_with(|| SigningKey::random(&mut OsRng)); - memory_key_info(&key_id, signing_key, self.class) - } - - fn delete( - &self, - key_id: &str, - protection_class: DeviceKeyProtectionClass, - ) -> Result<(), DeviceKeyError> { - if protection_class != self.class { - return Ok(()); - } - self.keys - .lock() - .map_err(|err| DeviceKeyError::Platform(err.to_string()))? - .remove(key_id); - Ok(()) - } - - fn get_public( - &self, - key_id: &str, - protection_class: DeviceKeyProtectionClass, - ) -> Result { - if protection_class != self.class { - return Err(DeviceKeyError::KeyNotFound); - } - let keys = self - .keys - .lock() - .map_err(|err| DeviceKeyError::Platform(err.to_string()))?; - let signing_key = keys.get(key_id).ok_or(DeviceKeyError::KeyNotFound)?; - memory_key_info(key_id, signing_key, self.class) - } - - fn sign( - &self, - key_id: &str, - protection_class: DeviceKeyProtectionClass, - payload: &[u8], - ) -> Result { - if protection_class != self.class { - return Err(DeviceKeyError::KeyNotFound); - } - let keys = self - .keys - .lock() - .map_err(|err| DeviceKeyError::Platform(err.to_string()))?; - let signing_key = keys.get(key_id).ok_or(DeviceKeyError::KeyNotFound)?; - let signature: Signature = signing_key.sign(payload); - Ok(ProviderSignature { - signature_der: signature.to_der().as_bytes().to_vec(), - algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256, - }) - } - } - - #[derive(Debug)] - struct FailingBindingStore; - - #[async_trait] - impl DeviceKeyBindingStore for FailingBindingStore { - async fn get_binding( - &self, - _key_id: &str, - ) -> Result, DeviceKeyError> { - Ok(None) - } - - async fn put_binding( - &self, - _key_id: &str, - _binding: &DeviceKeyBinding, - ) -> Result<(), DeviceKeyError> { - Err(DeviceKeyError::Platform("binding write failed".to_string())) - } - } - - fn memory_key_info( - key_id: &str, - signing_key: &SigningKey, - class: DeviceKeyProtectionClass, - ) -> Result { - let public_key_spki_der = signing_key - .verifying_key() - .to_public_key_der() - .map_err(|err| DeviceKeyError::Crypto(err.to_string()))? - .as_bytes() - .to_vec(); - Ok(DeviceKeyInfo { - key_id: key_id.to_string(), - public_key_spki_der, - algorithm: DeviceKeyAlgorithm::EcdsaP256Sha256, - protection_class: class, - }) - } - - fn store(class: DeviceKeyProtectionClass) -> DeviceKeyStore { - DeviceKeyStore::new_for_test(Arc::new(MemoryProvider::new(class))) - } - - fn block_on(future: impl std::future::Future) -> T { - tokio::runtime::Builder::new_current_thread() - .build() - .expect("build test runtime") - .block_on(future) - } - - fn create_request(protection_policy: DeviceKeyProtectionPolicy) -> DeviceKeyCreateRequest { - DeviceKeyCreateRequest { - protection_policy, - binding: DeviceKeyBinding { - account_user_id: "account-user-1".to_string(), - client_id: "cli_123".to_string(), - }, - } - } - - fn remote_control_client_connection_payload() -> DeviceKeySignPayload { - DeviceKeySignPayload::RemoteControlClientConnection( - RemoteControlClientConnectionSignPayload { - nonce: TEST_NONCE_BASE64URL.to_string(), - audience: RemoteControlClientConnectionAudience::RemoteControlClientWebsocket, - session_id: "wssess_123".to_string(), - target_origin: "https://chatgpt.com".to_string(), - target_path: "/api/codex/remote/control/client".to_string(), - account_user_id: "account-user-1".to_string(), - client_id: "cli_123".to_string(), - token_sha256_base64url: TEST_TOKEN_SHA256_BASE64URL.to_string(), - token_expires_at: current_unix_seconds().expect("time should be valid") + 60, - scopes: vec![REMOTE_CONTROL_CONTROLLER_WEBSOCKET_SCOPE.to_string()], - }, - ) - } - - fn remote_control_client_enrollment_payload() -> DeviceKeySignPayload { - DeviceKeySignPayload::RemoteControlClientEnrollment( - RemoteControlClientEnrollmentSignPayload { - nonce: TEST_NONCE_BASE64URL.to_string(), - audience: RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment, - challenge_id: "rch_123".to_string(), - target_origin: "https://chatgpt.com".to_string(), - target_path: "/wham/remote/control/client/enroll".to_string(), - account_user_id: "account-user-1".to_string(), - client_id: "cli_123".to_string(), - device_identity_sha256_base64url: TEST_TOKEN_SHA256_BASE64URL.to_string(), - challenge_expires_at: current_unix_seconds().expect("time should be valid") + 60, - }, - ) - } - - fn assert_valid_generated_key_id(key_id: &str, expected_class: DeviceKeyProtectionClass) { - assert_eq!(key_id.len(), DEVICE_KEY_ID_LEN); - assert_eq!( - validate_key_id(key_id).expect("generated key id should be valid"), - expected_class - ); - let encoded_key = key_id - .strip_prefix(expected_class.key_id_prefix()) - .expect("generated key id should use protection-class prefix"); - assert_eq!(encoded_key.len(), DEVICE_KEY_ID_ENCODED_BYTES); - assert_eq!( - URL_SAFE_NO_PAD - .decode(encoded_key) - .expect("generated key id should be base64url") - .len(), - DEVICE_KEY_ID_RANDOM_BYTES - ); - } - - #[test] - fn create_requires_explicit_degraded_protection() { - let err = block_on( - store(DeviceKeyProtectionClass::OsProtectedNonextractable) - .create(create_request(DeviceKeyProtectionPolicy::HardwareOnly)), - ) - .expect_err("OS-protected fallback should require opt-in"); - - assert!( - matches!( - err, - DeviceKeyError::DegradedProtectionNotAllowed { - available: DeviceKeyProtectionClass::OsProtectedNonextractable, - } - ), - "unexpected error: {err:?}" - ); - } - - #[test] - fn create_allows_os_protected_nonextractable_policy() { - let info = block_on( - store(DeviceKeyProtectionClass::OsProtectedNonextractable).create(create_request( - DeviceKeyProtectionPolicy::AllowOsProtectedNonextractable, - )), - ) - .expect("OS-protected fallback should be allowed by policy"); - - assert_eq!( - info.protection_class, - DeviceKeyProtectionClass::OsProtectedNonextractable - ); - assert_valid_generated_key_id( - &info.key_id, - DeviceKeyProtectionClass::OsProtectedNonextractable, - ); - } - - #[test] - fn create_generates_distinct_key_ids() { - let store = store(DeviceKeyProtectionClass::HardwareTpm); - let first = block_on(store.create(create_request(DeviceKeyProtectionPolicy::HardwareOnly))) - .expect("create should succeed"); - let second = - block_on(store.create(create_request(DeviceKeyProtectionPolicy::HardwareOnly))) - .expect("create should succeed"); - - assert_ne!(second.key_id, first.key_id); - assert_valid_generated_key_id(&first.key_id, DeviceKeyProtectionClass::HardwareTpm); - assert_valid_generated_key_id(&second.key_id, DeviceKeyProtectionClass::HardwareTpm); - } - - #[test] - fn create_deletes_provider_key_when_binding_write_fails() { - let provider = Arc::new(MemoryProvider::new(DeviceKeyProtectionClass::HardwareTpm)); - let store = DeviceKeyStore { - provider: provider.clone(), - bindings: Arc::new(FailingBindingStore), - }; - - let err = block_on(store.create(create_request(DeviceKeyProtectionPolicy::HardwareOnly))) - .expect_err("binding failure should fail create"); - - assert!( - matches!( - &err, - DeviceKeyError::Platform(message) if message == "binding write failed" - ), - "unexpected error: {err:?}" - ); - assert_eq!(provider.key_count(), 0); - } - - #[test] - fn key_id_validation_rejects_untrusted_namespaces() { - let valid_suffix = URL_SAFE_NO_PAD.encode([0_u8; DEVICE_KEY_ID_RANDOM_BYTES]); - - for key_id in [ - String::new(), - "dk_".to_string(), - "dk_hse_".to_string(), - format!("bad_{valid_suffix}"), - format!("dk_bad_{valid_suffix}"), - format!( - "{}{}", - DeviceKeyProtectionClass::HardwareSecureEnclave.key_id_prefix(), - &valid_suffix[..DEVICE_KEY_ID_ENCODED_BYTES - 1] - ), - format!( - "{}{valid_suffix}A", - DeviceKeyProtectionClass::HardwareTpm.key_id_prefix() - ), - format!( - "{}{}=", - DeviceKeyProtectionClass::OsProtectedNonextractable.key_id_prefix(), - &valid_suffix[..DEVICE_KEY_ID_ENCODED_BYTES - 1] - ), - format!( - "{}{}+", - DeviceKeyProtectionClass::HardwareSecureEnclave.key_id_prefix(), - &valid_suffix[..DEVICE_KEY_ID_ENCODED_BYTES - 1] - ), - ] { - let err = validate_key_id(&key_id).expect_err("malformed key id should fail"); - assert!( - matches!( - err, - DeviceKeyError::InvalidPayload(INVALID_DEVICE_KEY_ID_MESSAGE) - ), - "unexpected error for {key_id:?}: {err:?}" - ); - } - } - - #[test] - fn public_operations_reject_malformed_key_id_before_provider_use() { - let store = store(DeviceKeyProtectionClass::HardwareTpm); - let malformed_key_id = "not-a-device-key".to_string(); - - let err = block_on(store.get_public(DeviceKeyGetPublicRequest { - key_id: malformed_key_id.clone(), - })) - .expect_err("malformed get_public key id should fail"); - assert!( - matches!( - err, - DeviceKeyError::InvalidPayload(INVALID_DEVICE_KEY_ID_MESSAGE) - ), - "unexpected get_public error: {err:?}" - ); - - let err = block_on(store.sign(DeviceKeySignRequest { - key_id: malformed_key_id, - payload: remote_control_client_connection_payload(), - })) - .expect_err("malformed sign key id should fail"); - assert!( - matches!( - err, - DeviceKeyError::InvalidPayload(INVALID_DEVICE_KEY_ID_MESSAGE) - ), - "unexpected sign error: {err:?}" - ); - } - - #[test] - fn sign_rejects_empty_account_user_id() { - let store = store(DeviceKeyProtectionClass::HardwareTpm); - let info = block_on(store.create(create_request(DeviceKeyProtectionPolicy::HardwareOnly))) - .expect("create should succeed"); - let mut payload = remote_control_client_connection_payload(); - match &mut payload { - DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { - connection_payload.account_user_id.clear(); - } - DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), - } - - let err = block_on(store.sign(DeviceKeySignRequest { - key_id: info.key_id, - payload, - })) - .expect_err("empty account user id should fail"); - - assert!( - matches!( - err, - DeviceKeyError::InvalidPayload("accountUserId must not be empty") - ), - "unexpected error: {err:?}" - ); - } - - #[test] - fn sign_uses_structured_payload() { - let store = store(DeviceKeyProtectionClass::HardwareTpm); - let info = block_on(store.create(create_request(DeviceKeyProtectionPolicy::HardwareOnly))) - .expect("create should succeed"); - let payload = remote_control_client_connection_payload(); - let signed_payload = - device_key_signing_payload_bytes(&payload).expect("payload should serialize"); - let signature = block_on(store.sign(DeviceKeySignRequest { - key_id: info.key_id, - payload, - })) - .expect("sign should succeed"); - assert_eq!(signature.signed_payload, signed_payload); - - let verifying_key = VerifyingKey::from_public_key_der(&info.public_key_spki_der) - .expect("public key should decode"); - let signature = - Signature::from_der(&signature.signature_der).expect("signature should decode"); - verifying_key - .verify(&signed_payload, &signature) - .expect("signature should verify against structured payload"); - } - - #[test] - fn signing_payload_bytes_are_stable() { - let payload = DeviceKeySignPayload::RemoteControlClientConnection( - RemoteControlClientConnectionSignPayload { - nonce: TEST_NONCE_BASE64URL.to_string(), - audience: RemoteControlClientConnectionAudience::RemoteControlClientWebsocket, - session_id: "wssess_123".to_string(), - target_origin: "https://chatgpt.com".to_string(), - target_path: "/api/codex/remote/control/client".to_string(), - account_user_id: "account-user-1".to_string(), - client_id: "cli_123".to_string(), - token_sha256_base64url: TEST_TOKEN_SHA256_BASE64URL.to_string(), - token_expires_at: 1_700_000_000, - scopes: vec![REMOTE_CONTROL_CONTROLLER_WEBSOCKET_SCOPE.to_string()], - }, - ); - - let bytes = device_key_signing_payload_bytes(&payload).expect("payload should serialize"); - - assert_eq!( - String::from_utf8(bytes).expect("payload should be utf-8"), - concat!( - "{\"domain\":\"codex-device-key-sign-payload/v1\",", - "\"payload\":{\"accountUserId\":\"account-user-1\",", - "\"audience\":\"remote_control_client_websocket\",", - "\"clientId\":\"cli_123\",", - "\"nonce\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",", - "\"scopes\":[\"remote_control_controller_websocket\"],", - "\"sessionId\":\"wssess_123\",", - "\"targetOrigin\":\"https://chatgpt.com\",", - "\"targetPath\":\"/api/codex/remote/control/client\",", - "\"tokenExpiresAt\":1700000000,", - "\"tokenSha256Base64url\":\"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\",", - "\"type\":\"remoteControlClientConnection\"}}" - ) - ); - } - - #[test] - fn enrollment_signing_payload_bytes_are_stable() { - let payload = DeviceKeySignPayload::RemoteControlClientEnrollment( - RemoteControlClientEnrollmentSignPayload { - nonce: TEST_NONCE_BASE64URL.to_string(), - audience: RemoteControlClientEnrollmentAudience::RemoteControlClientEnrollment, - challenge_id: "rch_123".to_string(), - target_origin: "https://chatgpt.com".to_string(), - target_path: "/wham/remote/control/client/enroll".to_string(), - account_user_id: "account-user-1".to_string(), - client_id: "cli_123".to_string(), - device_identity_sha256_base64url: TEST_TOKEN_SHA256_BASE64URL.to_string(), - challenge_expires_at: 1_700_000_060, - }, - ); - - let bytes = device_key_signing_payload_bytes(&payload).expect("payload should serialize"); - - assert_eq!( - String::from_utf8(bytes).expect("payload should be utf-8"), - concat!( - "{\"domain\":\"codex-device-key-sign-payload/v1\",", - "\"payload\":{\"accountUserId\":\"account-user-1\",", - "\"audience\":\"remote_control_client_enrollment\",", - "\"challengeExpiresAt\":1700000060,", - "\"challengeId\":\"rch_123\",", - "\"clientId\":\"cli_123\",", - "\"deviceIdentitySha256Base64url\":\"47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU\",", - "\"nonce\":\"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\",", - "\"targetOrigin\":\"https://chatgpt.com\",", - "\"targetPath\":\"/wham/remote/control/client/enroll\",", - "\"type\":\"remoteControlClientEnrollment\"}}" - ) - ); - } - - #[test] - fn sign_rejects_malformed_token_hash() { - let store = store(DeviceKeyProtectionClass::HardwareTpm); - let info = block_on(store.create(create_request(DeviceKeyProtectionPolicy::HardwareOnly))) - .expect("create should succeed"); - let mut payload = remote_control_client_connection_payload(); - match &mut payload { - DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { - connection_payload.token_sha256_base64url = "not-a-sha256".to_string(); - } - DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), - } - - let err = block_on(store.sign(DeviceKeySignRequest { - key_id: info.key_id, - payload, - })) - .expect_err("malformed token hash should fail"); - - assert!( - matches!( - err, - DeviceKeyError::InvalidPayload( - "tokenSha256Base64url must be a SHA-256 digest encoded as unpadded base64url" - ) - ), - "unexpected error: {err:?}" - ); - } - - #[test] - fn sign_rejects_unexpected_scopes() { - let store = store(DeviceKeyProtectionClass::HardwareTpm); - let info = block_on(store.create(create_request(DeviceKeyProtectionPolicy::HardwareOnly))) - .expect("create should succeed"); - let mut payload = remote_control_client_connection_payload(); - match &mut payload { - DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { - connection_payload.scopes = vec!["other_scope".to_string()]; - } - DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), - } - - let err = block_on(store.sign(DeviceKeySignRequest { - key_id: info.key_id, - payload, - })) - .expect_err("unexpected scope should fail"); - - assert!( - matches!( - err, - DeviceKeyError::InvalidPayload( - "scopes must contain exactly remote_control_controller_websocket" - ) - ), - "unexpected error: {err:?}" - ); - } - - #[test] - fn sign_rejects_malformed_enrollment_identity_hash() { - let store = store(DeviceKeyProtectionClass::HardwareTpm); - let info = block_on(store.create(create_request(DeviceKeyProtectionPolicy::HardwareOnly))) - .expect("create should succeed"); - let mut payload = remote_control_client_enrollment_payload(); - match &mut payload { - DeviceKeySignPayload::RemoteControlClientEnrollment(enrollment_payload) => { - enrollment_payload.device_identity_sha256_base64url = "not-a-sha256".to_string(); - } - DeviceKeySignPayload::RemoteControlClientConnection(_) => unreachable!(), - } - - let err = block_on(store.sign(DeviceKeySignRequest { - key_id: info.key_id, - payload, - })) - .expect_err("malformed device identity hash should fail"); - - assert!( - matches!( - err, - DeviceKeyError::InvalidPayload( - "deviceIdentitySha256Base64url must be a SHA-256 digest encoded as unpadded base64url" - ) - ), - "unexpected error: {err:?}" - ); - } - - #[test] - fn sign_rejects_empty_target_binding() { - let store = store(DeviceKeyProtectionClass::HardwareTpm); - let info = block_on(store.create(create_request(DeviceKeyProtectionPolicy::HardwareOnly))) - .expect("create should succeed"); - let mut payload = remote_control_client_connection_payload(); - match &mut payload { - DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { - connection_payload.target_origin.clear(); - } - DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), - } - - let err = block_on(store.sign(DeviceKeySignRequest { - key_id: info.key_id, - payload, - })) - .expect_err("empty target origin should fail"); - - assert!( - matches!( - err, - DeviceKeyError::InvalidPayload( - "targetOrigin must be an allowed remote-control backend origin" - ) - ), - "unexpected error: {err:?}" - ); - } - - #[test] - fn sign_rejects_remote_control_paths_for_other_payload_shapes() { - let store = store(DeviceKeyProtectionClass::HardwareTpm); - let info = block_on(store.create(create_request(DeviceKeyProtectionPolicy::HardwareOnly))) - .expect("create should succeed"); - let mut connection_payload = remote_control_client_connection_payload(); - match &mut connection_payload { - DeviceKeySignPayload::RemoteControlClientConnection(payload) => { - payload.target_path = "/api/codex/remote/control/client/enroll".to_string(); - } - DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), - } - - let err = block_on(store.sign(DeviceKeySignRequest { - key_id: info.key_id.clone(), - payload: connection_payload, - })) - .expect_err("connection payload should reject enrollment path"); - assert!( - matches!( - err, - DeviceKeyError::InvalidPayload( - "targetPath must match the signed payload type's remote-control endpoint" - ) - ), - "unexpected connection path error: {err:?}" - ); - - let mut enrollment_payload = remote_control_client_enrollment_payload(); - match &mut enrollment_payload { - DeviceKeySignPayload::RemoteControlClientEnrollment(payload) => { - payload.target_path = "/wham/remote/control/client".to_string(); - } - DeviceKeySignPayload::RemoteControlClientConnection(_) => unreachable!(), - } - - let err = block_on(store.sign(DeviceKeySignRequest { - key_id: info.key_id, - payload: enrollment_payload, - })) - .expect_err("enrollment payload should reject connection path"); - assert!( - matches!( - err, - DeviceKeyError::InvalidPayload( - "targetPath must match the signed payload type's remote-control endpoint" - ) - ), - "unexpected enrollment path error: {err:?}" - ); - } - - #[test] - fn remote_control_origin_matches_remote_transport_allowlist() { - for origin in [ - "https://chatgpt.com", - "https://chatgpt-staging.com", - "https://ab.chatgpt.com", - "https://ab.chatgpt-staging.com", - "http://localhost:8080", - "https://localhost:8443", - "http://127.0.0.1:8080", - "http://[::1]:8080", - ] { - assert!( - is_allowed_remote_control_origin(origin), - "expected allowed origin: {origin}" - ); - } - - for origin in [ - "http://chatgpt.com", - "https://chat.openai.com", - "https://api.openai.com", - "https://chatgpt.com.evil.com", - "https://evilchatgpt.com", - "https://foo.localhost", - "https://localhost.evil.com", - "https://192.168.1.2", - "https://chatgpt.com/backend-api", - "https://chatgpt.com?query=1", - ] { - assert!( - !is_allowed_remote_control_origin(origin), - "expected rejected origin: {origin}" - ); - } - } - - #[test] - fn sign_rejects_empty_session_binding() { - let store = store(DeviceKeyProtectionClass::HardwareTpm); - let info = block_on(store.create(create_request(DeviceKeyProtectionPolicy::HardwareOnly))) - .expect("create should succeed"); - let mut payload = remote_control_client_connection_payload(); - match &mut payload { - DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { - connection_payload.session_id.clear(); - } - DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), - } - - let err = block_on(store.sign(DeviceKeySignRequest { - key_id: info.key_id, - payload, - })) - .expect_err("empty session id should fail"); - - assert!( - matches!( - err, - DeviceKeyError::InvalidPayload("sessionId must not be empty") - ), - "unexpected error: {err:?}" - ); - } - - #[test] - fn sign_rejects_empty_client_id() { - let store = store(DeviceKeyProtectionClass::HardwareTpm); - let info = block_on(store.create(create_request(DeviceKeyProtectionPolicy::HardwareOnly))) - .expect("create should succeed"); - let mut payload = remote_control_client_connection_payload(); - match &mut payload { - DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { - connection_payload.client_id.clear(); - } - DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), - } - - let err = block_on(store.sign(DeviceKeySignRequest { - key_id: info.key_id, - payload, - })) - .expect_err("empty client id should fail"); - - assert!( - matches!( - err, - DeviceKeyError::InvalidPayload("clientId must not be empty") - ), - "unexpected error: {err:?}" - ); - } - - #[test] - fn sign_rejects_mismatched_binding() { - let store = store(DeviceKeyProtectionClass::HardwareTpm); - let info = block_on(store.create(create_request(DeviceKeyProtectionPolicy::HardwareOnly))) - .expect("create should succeed"); - let mut payload = remote_control_client_connection_payload(); - match &mut payload { - DeviceKeySignPayload::RemoteControlClientConnection(connection_payload) => { - connection_payload.account_user_id = "other-account-user".to_string(); - } - DeviceKeySignPayload::RemoteControlClientEnrollment(_) => unreachable!(), - } - - let err = block_on(store.sign(DeviceKeySignRequest { - key_id: info.key_id, - payload, - })) - .expect_err("mismatched binding should fail"); - - assert!( - matches!( - err, - DeviceKeyError::InvalidPayload( - "payload accountUserId/clientId does not match device key binding" - ) - ), - "unexpected error: {err:?}" - ); - } -} diff --git a/codex-rs/device-key/src/platform.rs b/codex-rs/device-key/src/platform.rs deleted file mode 100644 index 60a2f508364b..000000000000 --- a/codex-rs/device-key/src/platform.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::DeviceKeyError; -use crate::DeviceKeyInfo; -use crate::DeviceKeyProtectionClass; -use crate::DeviceKeyProvider; -use crate::ProviderCreateRequest; -use crate::ProviderSignature; -use std::sync::Arc; - -pub(crate) fn default_provider() -> Arc { - Arc::new(UnsupportedDeviceKeyProvider) -} - -#[derive(Debug)] -pub(crate) struct UnsupportedDeviceKeyProvider; - -impl DeviceKeyProvider for UnsupportedDeviceKeyProvider { - fn create(&self, request: ProviderCreateRequest) -> Result { - let _ = request.key_id_for(DeviceKeyProtectionClass::HardwareTpm); - let _ = request - .protection_policy - .allows(DeviceKeyProtectionClass::HardwareTpm); - Err(DeviceKeyError::HardwareBackedKeysUnavailable) - } - - fn delete( - &self, - _key_id: &str, - _protection_class: DeviceKeyProtectionClass, - ) -> Result<(), DeviceKeyError> { - Ok(()) - } - - fn get_public( - &self, - _key_id: &str, - _protection_class: DeviceKeyProtectionClass, - ) -> Result { - Err(DeviceKeyError::KeyNotFound) - } - - fn sign( - &self, - _key_id: &str, - _protection_class: DeviceKeyProtectionClass, - _payload: &[u8], - ) -> Result { - Err(DeviceKeyError::KeyNotFound) - } -} diff --git a/codex-rs/state/migrations/0031_drop_device_key_bindings.sql b/codex-rs/state/migrations/0031_drop_device_key_bindings.sql new file mode 100644 index 000000000000..7b40b11edfbb --- /dev/null +++ b/codex-rs/state/migrations/0031_drop_device_key_bindings.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS device_key_bindings; diff --git a/codex-rs/state/src/lib.rs b/codex-rs/state/src/lib.rs index 005cfa495876..84582370a5af 100644 --- a/codex-rs/state/src/lib.rs +++ b/codex-rs/state/src/lib.rs @@ -47,7 +47,6 @@ pub use model::ThreadGoalStatus; pub use model::ThreadMetadata; pub use model::ThreadMetadataBuilder; pub use model::ThreadsPage; -pub use runtime::DeviceKeyBindingRecord; pub use runtime::RemoteControlEnrollmentRecord; pub use runtime::ThreadFilterOptions; pub use runtime::ThreadGoalAccountingMode; diff --git a/codex-rs/state/src/runtime.rs b/codex-rs/state/src/runtime.rs index 2dc24788d87f..c8b4e7b98e2f 100644 --- a/codex-rs/state/src/runtime.rs +++ b/codex-rs/state/src/runtime.rs @@ -56,9 +56,6 @@ use tracing::warn; mod agent_jobs; mod backfill; -mod device_key; -#[cfg(test)] -mod device_key_tests; mod goals; mod logs; mod memories; @@ -67,7 +64,6 @@ mod remote_control; mod test_support; mod threads; -pub use device_key::DeviceKeyBindingRecord; pub use goals::ThreadGoalAccountingMode; pub use goals::ThreadGoalAccountingOutcome; pub use goals::ThreadGoalUpdate; diff --git a/codex-rs/state/src/runtime/device_key.rs b/codex-rs/state/src/runtime/device_key.rs deleted file mode 100644 index bb3f20f75903..000000000000 --- a/codex-rs/state/src/runtime/device_key.rs +++ /dev/null @@ -1,66 +0,0 @@ -use super::*; - -/// Persisted account/client binding for a generated device key. -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct DeviceKeyBindingRecord { - pub key_id: String, - pub account_user_id: String, - pub client_id: String, -} - -impl StateRuntime { - pub async fn get_device_key_binding( - &self, - key_id: &str, - ) -> anyhow::Result> { - let row = sqlx::query( - r#" -SELECT key_id, account_user_id, client_id -FROM device_key_bindings -WHERE key_id = ? - "#, - ) - .bind(key_id) - .fetch_optional(self.pool.as_ref()) - .await?; - - row.map(|row| { - Ok(DeviceKeyBindingRecord { - key_id: row.try_get("key_id")?, - account_user_id: row.try_get("account_user_id")?, - client_id: row.try_get("client_id")?, - }) - }) - .transpose() - } - - pub async fn upsert_device_key_binding( - &self, - binding: &DeviceKeyBindingRecord, - ) -> anyhow::Result<()> { - let now = Utc::now().timestamp(); - sqlx::query( - r#" -INSERT INTO device_key_bindings ( - key_id, - account_user_id, - client_id, - created_at, - updated_at -) VALUES (?, ?, ?, ?, ?) -ON CONFLICT(key_id) DO UPDATE SET - account_user_id = excluded.account_user_id, - client_id = excluded.client_id, - updated_at = excluded.updated_at - "#, - ) - .bind(&binding.key_id) - .bind(&binding.account_user_id) - .bind(&binding.client_id) - .bind(now) - .bind(now) - .execute(self.pool.as_ref()) - .await?; - Ok(()) - } -} diff --git a/codex-rs/state/src/runtime/device_key_tests.rs b/codex-rs/state/src/runtime/device_key_tests.rs deleted file mode 100644 index a29eaea94bd8..000000000000 --- a/codex-rs/state/src/runtime/device_key_tests.rs +++ /dev/null @@ -1,89 +0,0 @@ -use super::DeviceKeyBindingRecord; -use super::StateRuntime; -use super::test_support::unique_temp_dir; -use pretty_assertions::assert_eq; - -#[tokio::test] -async fn device_key_binding_round_trips_by_key_id() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string()) - .await - .expect("initialize runtime"); - - let first = DeviceKeyBindingRecord { - key_id: "dk_tpm_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(), - account_user_id: "account-user-a".to_string(), - client_id: "cli_a".to_string(), - }; - let second = DeviceKeyBindingRecord { - key_id: "dk_tpm_BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB".to_string(), - account_user_id: "account-user-b".to_string(), - client_id: "cli_b".to_string(), - }; - - runtime - .upsert_device_key_binding(&first) - .await - .expect("insert first binding"); - runtime - .upsert_device_key_binding(&second) - .await - .expect("insert second binding"); - - assert_eq!( - runtime - .get_device_key_binding(&first.key_id) - .await - .expect("load first binding"), - Some(first) - ); - assert_eq!( - runtime - .get_device_key_binding("dk_tpm_missing") - .await - .expect("load missing binding"), - None - ); - - let _ = tokio::fs::remove_dir_all(codex_home).await; -} - -#[tokio::test] -async fn device_key_binding_upsert_updates_existing_binding() { - let codex_home = unique_temp_dir(); - let runtime = StateRuntime::init(codex_home.clone(), "test-provider".to_string()) - .await - .expect("initialize runtime"); - - let key_id = "dk_tpm_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(); - runtime - .upsert_device_key_binding(&DeviceKeyBindingRecord { - key_id: key_id.clone(), - account_user_id: "account-user-a".to_string(), - client_id: "cli_a".to_string(), - }) - .await - .expect("insert binding"); - runtime - .upsert_device_key_binding(&DeviceKeyBindingRecord { - key_id: key_id.clone(), - account_user_id: "account-user-b".to_string(), - client_id: "cli_b".to_string(), - }) - .await - .expect("update binding"); - - assert_eq!( - runtime - .get_device_key_binding(&key_id) - .await - .expect("load updated binding"), - Some(DeviceKeyBindingRecord { - key_id, - account_user_id: "account-user-b".to_string(), - client_id: "cli_b".to_string(), - }) - ); - - let _ = tokio::fs::remove_dir_all(codex_home).await; -}