4 Commits
Author SHA1 Message Date
ZacharyZhang-NY a009446fdb docs: add AGit pull request instructions 2026-10-09 19:46:37 +00:00
雷电芽衣 6c39ce7781 chore(go): release 2.1.1
Carries the non-idempotent retry guard, previous_token_grace_until, and
agent-id path escaping.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 21:47:57 -04:00
雷电芽衣 b84c79816c docs: drop the em-dash from the genesis hash comment
Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 21:07:25 -04:00
雷电芽衣 ce0c924c58 fix: stop replaying non-idempotent requests and expose the rotation grace
A retried POST /v1/auth/rotate loses the replacement token because the
presented token is already in grace, and a retried webhook registration
creates duplicates. Only idempotent methods now retry. Add
previous_token_grace_until to RotateApiTokenResponse and percent-encode
the agent id in request paths.

Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
2026-09-03 20:12:48 -04:00
6 changed files with 42 additions and 10 deletions

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
# Contributing
## Opening a pull request on git.zacharyzhang.com
Forking is disabled on this server. Open pull requests with AGit instead:
1. Register an account at https://git.zacharyzhang.com
2. Clone, commit your changes, then push to a `refs/for/<target-branch>` ref:
```bash
git clone https://git.zacharyzhang.com/Elydora-Infrastructure/Elydora-Go-SDK.git
git push origin HEAD:refs/for/main -o topic=<short-name> -o title="<PR title>" -o description="<PR body>"
```
3. To update the pull request, push again with the same `topic`.
+1 -1
View File
@@ -309,7 +309,7 @@ deep, err := elydora.DeepHealth("https://api.elydora.com")
### Constants
```go
// Genesis chain hash — initial prev_chain_hash for an agent's first operation
// Genesis chain hash: initial prev_chain_hash for an agent's first operation
elydora.GenesisChainHash // "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
```
+7 -6
View File
@@ -3,6 +3,7 @@ package elydora
import (
"fmt"
"net/http"
"net/url"
)
// RegisterAgent registers a new AI agent within the organization.
@@ -17,7 +18,7 @@ func (c *Client) RegisterAgent(req *RegisterAgentRequest) (*RegisterAgentRespons
// GetAgent retrieves an agent by ID.
func (c *Client) GetAgent(agentID string) (*GetAgentResponse, error) {
var result GetAgentResponse
if err := c.doGet(fmt.Sprintf("/v1/agents/%s", agentID), &result); err != nil {
if err := c.doGet(fmt.Sprintf("/v1/agents/%s", url.PathEscape(agentID)), &result); err != nil {
return nil, err
}
return &result, nil
@@ -26,7 +27,7 @@ func (c *Client) GetAgent(agentID string) (*GetAgentResponse, error) {
// FreezeAgent freezes an agent, preventing it from submitting operations.
func (c *Client) FreezeAgent(agentID, reason string) (*FreezeAgentResponse, error) {
var result FreezeAgentResponse
if err := c.doPost(fmt.Sprintf("/v1/agents/%s/freeze", agentID), &FreezeAgentRequest{
if err := c.doPost(fmt.Sprintf("/v1/agents/%s/freeze", url.PathEscape(agentID)), &FreezeAgentRequest{
Reason: reason,
}, &result); err != nil {
return nil, err
@@ -36,7 +37,7 @@ func (c *Client) FreezeAgent(agentID, reason string) (*FreezeAgentResponse, erro
// RevokeKey revokes a specific key for an agent.
func (c *Client) RevokeKey(agentID, kid, reason string) error {
return c.doPost(fmt.Sprintf("/v1/agents/%s/revoke", agentID), &RevokeAgentRequest{
return c.doPost(fmt.Sprintf("/v1/agents/%s/revoke", url.PathEscape(agentID)), &RevokeAgentRequest{
KID: kid,
Reason: reason,
}, nil)
@@ -54,7 +55,7 @@ func (c *Client) ListAgents() (*ListAgentsResponse, error) {
// UnfreezeAgent unfreezes a previously frozen agent.
func (c *Client) UnfreezeAgent(agentID, reason string) (*UnfreezeAgentResponse, error) {
var result UnfreezeAgentResponse
if err := c.doPost(fmt.Sprintf("/v1/agents/%s/unfreeze", agentID), &UnfreezeAgentRequest{
if err := c.doPost(fmt.Sprintf("/v1/agents/%s/unfreeze", url.PathEscape(agentID)), &UnfreezeAgentRequest{
Reason: reason,
}, &result); err != nil {
return nil, err
@@ -65,7 +66,7 @@ func (c *Client) UnfreezeAgent(agentID, reason string) (*UnfreezeAgentResponse,
// UpdateAgent updates an agent's integration type.
func (c *Client) UpdateAgent(agentID string, integrationType IntegrationType) (*UpdateAgentResponse, error) {
var result UpdateAgentResponse
if err := c.doRequest(http.MethodPatch, fmt.Sprintf("/v1/agents/%s", agentID), &UpdateAgentRequest{
if err := c.doRequest(http.MethodPatch, fmt.Sprintf("/v1/agents/%s", url.PathEscape(agentID)), &UpdateAgentRequest{
IntegrationType: integrationType,
}, &result); err != nil {
return nil, err
@@ -76,7 +77,7 @@ func (c *Client) UpdateAgent(agentID string, integrationType IntegrationType) (*
// DeleteAgent permanently deletes an agent and all associated data.
func (c *Client) DeleteAgent(agentID string) (*DeleteAgentResponse, error) {
var result DeleteAgentResponse
if err := c.doRequest(http.MethodDelete, fmt.Sprintf("/v1/agents/%s", agentID), nil, &result); err != nil {
if err := c.doRequest(http.MethodDelete, fmt.Sprintf("/v1/agents/%s", url.PathEscape(agentID)), nil, &result); err != nil {
return nil, err
}
return &result, nil
+17 -2
View File
@@ -151,7 +151,16 @@ func retryable(status int) bool {
return status >= 500 || status == http.StatusTooManyRequests
}
// doRequest executes an authenticated request, retrying transport and 5xx failures.
// A lost response to a non-idempotent request must not be replayed.
func idempotent(method string) bool {
switch method {
case http.MethodGet, http.MethodHead, http.MethodOptions, http.MethodPut, http.MethodDelete:
return true
}
return false
}
// doRequest executes an authenticated request; only idempotent methods retry.
func (c *Client) doRequest(method, path string, body interface{}, result interface{}) error {
data, err := marshalBody(body)
if err != nil {
@@ -169,18 +178,24 @@ func (c *Client) doRequest(method, path string, body interface{}, result interfa
resp, err := c.httpClient.Do(req)
if err != nil {
lastErr = fmt.Errorf("elydora: http request: %w", err)
if !idempotent(method) {
return lastErr
}
continue
}
respBody, err := readBody(resp, maxResponseBytes)
if err != nil {
lastErr = err
if !idempotent(method) {
return lastErr
}
continue
}
if resp.StatusCode >= 200 && resp.StatusCode < 300 {
return decodeSuccess(respBody, result)
}
lastErr = apiError(resp.StatusCode, respBody)
if !retryable(resp.StatusCode) {
if !retryable(resp.StatusCode) || !idempotent(method) {
return lastErr
}
}
+1 -1
View File
@@ -11,7 +11,7 @@ import (
"github.com/Elydora-Infrastructure/Elydora-Go-SDK/v2/cmd/elydora/plugins"
)
const version = "2.1.0"
const version = "2.1.1"
func main() {
if len(os.Args) < 2 {
+1
View File
@@ -82,6 +82,7 @@ type RotateApiTokenResponse struct {
Token string `json:"token"`
ExpiresAt *int64 `json:"expires_at"`
TokenID string `json:"token_id"`
PreviousTokenGraceUntil int64 `json:"previous_token_grace_until"`
}
type HealthResponse struct {