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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- iot-client — `iot_client_get_session_token_ex()`, which reports *why* the
cloud refused to issue an agent token.
- The refusals a device actually meets in the field all arrive on this one
path and need opposite handling: `GATEWAY_NOT_EXISTS` (removed from the
cloud — re-provision), `CHILD_PRIVACY_AGREEMENT_REQUIRED` (agreement not
signed yet — wait and retry, the user is expected to act in the app),
`ISSUE_TOKEN_FAILED` (no agent configured for the product — retrying is
pointless). `iot_client_get_session_token()` collapsed all three into
`OPRT_ATOP_BUSINESS_ERROR`, so a device could only retry blindly.
- `atop_base_response_t` already parsed `errorCode`/`errorMsg`, but
`atop_ai_token_get()` dropped them on the floor. They now reach the caller
through a new `iot_atop_rejection_t` out-parameter.
- Additive: `iot_client_get_session_token()` keeps its signature and
behaviour, and is now a wrapper passing `NULL`. Passing `NULL` for
`rejection` is supported and means "don't care".

- iot-client — device-initiated reset (`iot_client_reset`), for a device that
unbinds itself rather than waiting to be removed from the app.
- New public `iot_client_reset()` in `iot_client.h` over a new
Expand Down
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,7 @@ if(AGENTIC_KIT_BUILD_TESTS AND AGENTIC_KIT_ENABLE_PROJECT_TESTS)
agentic_kit_add_iot_test(iot_dns_test "${IOT_CLIENT_TESTS_DIR}/dns_test.c")
agentic_kit_add_iot_test(iot_atop_test "${IOT_CLIENT_TESTS_DIR}/atop_test.c")
agentic_kit_add_iot_test(iot_atop_call_test "${IOT_CLIENT_TESTS_DIR}/iot_atop_call_test.c")
agentic_kit_add_iot_test(iot_session_token_test "${IOT_CLIENT_TESTS_DIR}/iot_session_token_test.c")
agentic_kit_add_iot_test(iot_reset_test "${IOT_CLIENT_TESTS_DIR}/iot_reset_test.c")
agentic_kit_add_iot_test(iot_mqtt_test "${IOT_CLIENT_TESTS_DIR}/mqtt_test.c")
agentic_kit_add_iot_test(iot_message_test "${IOT_CLIENT_TESTS_DIR}/iot_client_message_test.c")
Expand Down
6 changes: 3 additions & 3 deletions modules/iot-client/include/iot_atop.h
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@
extern "C" {
#endif

/** Buffer sizes for the cloud's error strings (see iot_atop_response_t). */
#define IOT_ATOP_ERROR_CODE_LEN 48
#define IOT_ATOP_ERROR_MSG_LEN 128
/* IOT_ATOP_ERROR_CODE_LEN / IOT_ATOP_ERROR_MSG_LEN live in iot_client.h, next to
* iot_atop_rejection_t: the typed wrappers there report rejections too, and a
* header cannot include this one back (iot_atop.h includes iot_client.h). */

/**
* @brief What to call.
Expand Down
44 changes: 44 additions & 0 deletions modules/iot-client/include/iot_client.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,27 @@ typedef enum {
TEST,
} iot_env_t;

/** Buffer sizes for the cloud's rejection strings (see iot_atop_rejection_t). */
#define IOT_ATOP_ERROR_CODE_LEN 48
#define IOT_ATOP_ERROR_MSG_LEN 128

/**
* @brief Why the cloud rejected a call.
*
* A rejection is the cloud reaching a verdict and saying no: the envelope is
* well-formed, `errorCode` takes the place of `result`. The code is the
* discriminator the caller acts on — the same rejection surfaces as
* OPRT_ATOP_BUSINESS_ERROR no matter *why* the cloud refused, and those whys
* need opposite responses (a device removed from the cloud should re-provision;
* a product whose privacy agreement is unsigned must NOT).
*
* `code` is "" unless the call came back OPRT_ATOP_BUSINESS_ERROR.
*/
typedef struct {
char code[IOT_ATOP_ERROR_CODE_LEN]; /**< cloud errorCode, e.g. "GATEWAY_NOT_EXISTS" */
char msg[IOT_ATOP_ERROR_MSG_LEN]; /**< cloud errorMsg, human-readable */
} iot_atop_rejection_t;

/**
* @brief Initialize IoT SDK with the built-in default PAL adapter (POSIX / FreeRTOS).
*
Expand Down Expand Up @@ -387,6 +408,29 @@ IOT_API int iot_client_publish(iot_client_t *client, const uint8_t *data, size_t
*/
IOT_API int iot_client_get_session_token(iot_client_t *client, const char *agent_code, char *token, size_t token_len);

/**
* @brief Get an AI agent session token, and learn why the cloud said no.
*
* Same as iot_client_get_session_token(), except that a rejection is reported
* instead of being flattened into a bare OPRT_ATOP_BUSINESS_ERROR. Three
* different causes reach this call in practice — the device removed from the
* cloud, a product whose privacy agreement is unsigned, and a product with no
* AI agent configured — and they need opposite handling, so the code has to
* reach the caller that knows the product.
*
* @param client Pointer to iot_client_t instance
* @param agent_code Agent code string (NULL for default)
* @param token Output buffer for the token
* @param token_len Size of the output buffer in bytes
* @param rejection Optional; zeroed on entry and filled when the return code
* is OPRT_ATOP_BUSINESS_ERROR. NULL to ignore.
* @return OPRT_OK on success, OPRT_INVALID_PARAMETER if client or token is NULL,
* OPRT_ATOP_BUSINESS_ERROR when the cloud refused (see @p rejection).
*/
IOT_API int iot_client_get_session_token_ex(iot_client_t *client, const char *agent_code,
char *token, size_t token_len,
iot_atop_rejection_t *rejection);

/**
* @brief Get CA certificate for a target host via IoT DNS service.
*
Expand Down
8 changes: 8 additions & 0 deletions modules/iot-client/src/atop.c
Original file line number Diff line number Diff line change
Expand Up @@ -543,6 +543,14 @@ int atop_ai_token_get(const pal_t *pal, const ai_token_request_t *request, ai_to
pal->free(post_data);

if (rt != OPRT_OK) {
/* Carry the cloud's verdict out. Without this the caller sees only
* OPRT_ATOP_BUSINESS_ERROR, and "device removed from the cloud",
* "privacy agreement unsigned" and "no agent configured" become the
* same number — they need opposite handling. */
snprintf(response->rejection.code, sizeof(response->rejection.code), "%s",
atop_response.error_code);
snprintf(response->rejection.msg, sizeof(response->rejection.msg), "%s",
atop_response.error_msg);
log_error("http post err, rt:%d", rt);
atop_base_response_free(pal,&atop_response);
return rt;
Expand Down
1 change: 1 addition & 0 deletions modules/iot-client/src/atop.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ void atop_activate_response_free(const pal_t *pal, activite_response_t *response
*/
typedef struct {
char *token; // JSON string (caller must free)
iot_atop_rejection_t rejection; // filled when the cloud refused; code "" otherwise
} ai_token_response_t;

/**
Expand Down
14 changes: 14 additions & 0 deletions modules/iot-client/src/iot_client.c
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,17 @@ IOT_API iot_client_t *iot_client_init_on_boarding_with_token(const iot_on_boardi

IOT_API int iot_client_get_session_token(iot_client_t *client, const char *agent_code, char *token, size_t token_len)
{
return iot_client_get_session_token_ex(client, agent_code, token, token_len, NULL);
}

IOT_API int iot_client_get_session_token_ex(iot_client_t *client, const char *agent_code,
char *token, size_t token_len,
iot_atop_rejection_t *rejection)
{
if (rejection != NULL) {
memset(rejection, 0, sizeof(*rejection));
}

if (client == NULL || token == NULL || token_len == 0) {
log_error("iot_client_get_session_token: invalid parameters");
return OPRT_INVALID_PARAMETER;
Expand All @@ -591,6 +602,9 @@ IOT_API int iot_client_get_session_token(iot_client_t *client, const char *agent
ai_token_response_t resp = {0};
int ret = atop_ai_token_get(client->pal, &req, &resp);
if (ret != OPRT_OK) {
if (rejection != NULL) {
*rejection = resp.rejection;
}
log_error("atop_ai_token_get failed: %d", ret);
return ret;
}
Expand Down
Loading
Loading