You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
fix(api): agent_action URLs must use api-host for /api/v1 paths (#213)
Eleven agent_action strings shipped pointing /api/v1 calls at the
marketing host (https://instanode.dev/api/v1/...), which returns HTML
404 on every path. The canonical API host is api.instanode.dev. Every
LLM agent that hit one of these walls relayed a non-working URL to the
user, who then wasted a curl/POST attempt.
Live verification of the bug surface:
- curl https://instanode.dev/api/v1/resources -> HTTP/2 404 (HTML)
- curl https://api.instanode.dev/api/v1/resources -> HTTP/2 401 (JSON)
Fix:
- Flip 11 sites in agent_action.go (3) + helpers.go (8) to
https://api.instanode.dev/api/v1/...
- Relax assertContract URL check to accept either canonical host
(marketing https://instanode.dev/ for dashboard URLs, api host for
programmatic API paths). Docblock now spells out the two surfaces.
- Add TestAgentActionContract_APIPathsUseAPIHost — registry-iterating
regression test that asserts no /api/v path appears on the marketing
host. Iterates the live contract registry plus the long-form
deploy-TTL builder (which was already correct but had been excluded
from the contract gate for length). Verified to fail-fast on any
re-introduction of the bug (rule 18).
- Update one collateral test assertion in resource_pause_test.go to
use the dual-host check so AgentActionResourceAlreadyPaused still
satisfies its per-handler URL assertion.
Coverage (CLAUDE.md rule 17):
Symptom: agent_action returns "POST https://instanode.dev/api/v1/..."
-> HTML 404 on the marketing site
Enumeration: rg -nF 'https://instanode.dev/api/' internal/ -> 11 sites
Sites found: 11
Sites touched: 11
Coverage test: TestAgentActionContract_APIPathsUseAPIHost (registry-iterating)
Live verified: curl host-comparison above
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: internal/handlers/agent_action.go
+3-3Lines changed: 3 additions & 3 deletions
Original file line number
Diff line number
Diff line change
@@ -97,7 +97,7 @@ const AgentActionPauseRequiresPro = "Tell the user pausing resources requires th
97
97
// when the row is already in 'paused' state. The remedy is "do nothing"
98
98
// (the resource is in the requested state) or call /resume to flip back —
99
99
// both of which the action verb covers via "Have them".
100
-
constAgentActionResourceAlreadyPaused="Tell the user this resource is already paused. Have them call POST https://instanode.dev/api/v1/resources/:id/resume to bring it back online."
100
+
constAgentActionResourceAlreadyPaused="Tell the user this resource is already paused. Have them call POST https://api.instanode.dev/api/v1/resources/:id/resume to bring it back online."
101
101
102
102
// AgentActionResourceNotPaused is returned by POST /resources/:id/resume when
103
103
// the row isn't in 'paused' state — typically because it's already active.
@@ -298,7 +298,7 @@ const AgentActionBindingFamilyDisabled = "Tell the user this server has family b
"Tell the user the resource referenced in resource_bindings.%s doesn't exist. Have them list their families with GET https://instanode.dev/api/v1/resources/families and use a valid root id.",
301
+
"Tell the user the resource referenced in resource_bindings.%s doesn't exist. Have them list their families with GET https://api.instanode.dev/api/v1/resources/families and use a valid root id.",
"Tell the user to provision a %s twin of %q first: POST https://instanode.dev/api/v1/resources/%s/provision-twin with {\"env\":\"%s\"}. The deploy targets env=%s but no family member exists there.",
324
+
"Tell the user to provision a %s twin of %q first: POST https://api.instanode.dev/api/v1/resources/%s/provision-twin with {\"env\":\"%s\"}. The deploy targets env=%s but no family member exists there.",
"%s: agent_action mentions an /api/v path on the marketing host (https://instanode.dev/api/v...) — that URL returns HTML 404. Switch to https://api.instanode.dev/api/v... Got: %q",
264
+
name, s)
265
+
266
+
// Sanity: if the string mentions "/api/v" at all, it must use
267
+
// the api-host. This catches a hypothetical future bug where
268
+
// someone writes a bare "instanode.dev/api/v" (no scheme) — the
269
+
// NotContains above would miss it but the substring check below
270
+
// catches the structural mistake.
271
+
ifstrings.Contains(s, "/api/v") {
272
+
assert.Contains(t, s, rightHostAPIPrefix,
273
+
"%s: agent_action mentions an /api/v path but does not use the canonical api-host (%s...). Got: %q",
274
+
name, rightHostAPIPrefix, s)
275
+
}
276
+
})
277
+
}
278
+
}
279
+
208
280
// TestAgentActionContract_RegistryCoverage guards against the most likely
209
281
// regression: someone adds a new code to codeToAgentAction but its string
210
282
// silently fails the contract. The map iteration in
Copy file name to clipboardExpand all lines: internal/handlers/helpers.go
+8-8Lines changed: 8 additions & 8 deletions
Original file line number
Diff line number
Diff line change
@@ -154,7 +154,7 @@ var codeToAgentAction = map[string]errorCodeMeta{
154
154
UpgradeURL: "",
155
155
},
156
156
"no_existing_deployment_to_redeploy": {
157
-
AgentAction: "Tell the user no deployment with that name exists on this team. Omit redeploy=true to create one fresh, or list https://instanode.dev/api/v1/deployments to find the app_id and call POST /deploy/{id}/redeploy.",
157
+
AgentAction: "Tell the user no deployment with that name exists on this team. Omit redeploy=true to create one fresh, or list https://api.instanode.dev/api/v1/deployments to find the app_id and call POST /deploy/{id}/redeploy.",
158
158
UpgradeURL: "",
159
159
},
160
160
"rate_limit_exceeded": {
@@ -311,10 +311,10 @@ var codeToAgentAction = map[string]errorCodeMeta{
311
311
AgentAction: "Tell the user the team needs at least one owner. Have them promote another member to owner at https://instanode.dev/app/team before changing or removing this one.",
312
312
},
313
313
"cannot_remove_primary": {
314
-
AgentAction: "Tell the user they can't remove the primary user — every team needs a primary. Have them promote another member first via POST https://instanode.dev/api/v1/team/members/<other_user_id>/promote-to-primary, then retry the removal.",
314
+
AgentAction: "Tell the user they can't remove the primary user — every team needs a primary. Have them promote another member first via POST https://api.instanode.dev/api/v1/team/members/<other_user_id>/promote-to-primary, then retry the removal.",
315
315
},
316
316
"cannot_assign_owner_role": {
317
-
AgentAction: "Tell the user the owner role can't be assigned via PATCH role — ownership transfers atomically. Have them call POST https://instanode.dev/api/v1/team/members/<user_id>/promote-to-primary instead.",
317
+
AgentAction: "Tell the user the owner role can't be assigned via PATCH role — ownership transfers atomically. Have them call POST https://api.instanode.dev/api/v1/team/members/<user_id>/promote-to-primary instead.",
@@ -502,7 +502,7 @@ var codeToAgentAction = map[string]errorCodeMeta{
502
502
AgentAction: "Tell the user one or more required fields are missing. Check the response message for the field list and retry — see https://instanode.dev/docs.",
503
503
},
504
504
"missing_backup_id": {
505
-
AgentAction: "Tell the user the backup_id path parameter is missing. Use GET https://instanode.dev/api/v1/backups to find an id and retry.",
505
+
AgentAction: "Tell the user the backup_id path parameter is missing. Use GET https://api.instanode.dev/api/v1/backups to find an id and retry.",
506
506
},
507
507
"missing_confirm_slug": {
508
508
AgentAction: "Tell the user the confirm_slug field is required to confirm this destructive action — supply the slug exactly as shown in the prompt and retry — see https://instanode.dev/docs.",
@@ -657,13 +657,13 @@ var codeToAgentAction = map[string]errorCodeMeta{
657
657
AgentAction: "Tell the user the approval_id is not a valid UUID. Check the approval link in your email and retry — see https://instanode.dev/docs/promote.",
658
658
},
659
659
"invalid_backup_id": {
660
-
AgentAction: "Tell the user the backup_id is not a valid UUID. List backups at GET https://instanode.dev/api/v1/backups and retry.",
660
+
AgentAction: "Tell the user the backup_id is not a valid UUID. List backups at GET https://api.instanode.dev/api/v1/backups and retry.",
661
661
},
662
662
"invalid_target": {
663
663
AgentAction: "Tell the user the target value is invalid. Check the docs at https://instanode.dev/docs for the allowed targets.",
664
664
},
665
665
"invalid_target_resource_id": {
666
-
AgentAction: "Tell the user the target_resource_id is not a valid UUID. List resources at GET https://instanode.dev/api/v1/resources and retry.",
666
+
AgentAction: "Tell the user the target_resource_id is not a valid UUID. List resources at GET https://api.instanode.dev/api/v1/resources and retry.",
667
667
},
668
668
"invalid_parent_resource_id": {
669
669
AgentAction: "Tell the user the parent_resource_id is not a valid UUID. Check the resource list at https://instanode.dev/app/resources and retry.",
@@ -719,7 +719,7 @@ var codeToAgentAction = map[string]errorCodeMeta{
719
719
AgentAction: "Tell the user the parent resource referenced by this request no longer exists. Re-provision the parent or retarget — see https://instanode.dev/docs.",
720
720
},
721
721
"backup_not_found": {
722
-
AgentAction: "Tell the user the backup id is unknown. List available backups at GET https://instanode.dev/api/v1/backups and retry.",
722
+
AgentAction: "Tell the user the backup id is unknown. List available backups at GET https://api.instanode.dev/api/v1/backups and retry.",
723
723
},
724
724
"approval_not_found": {
725
725
AgentAction: "Tell the user the approval link is invalid or expired. The team owner can re-issue the approval — see https://instanode.dev/docs/promote.",
@@ -867,7 +867,7 @@ var codeToAgentAction = map[string]errorCodeMeta{
867
867
},
868
868
// (invitation_invalid covered in the auth/token section above)
869
869
"backup_resource_mismatch": {
870
-
AgentAction: "Tell the user this backup belongs to a different resource. List the resource's backups at GET https://instanode.dev/api/v1/resources/<id>/backups and retry.",
870
+
AgentAction: "Tell the user this backup belongs to a different resource. List the resource's backups at GET https://api.instanode.dev/api/v1/resources/<id>/backups and retry.",
871
871
},
872
872
"restore_in_progress": {
873
873
AgentAction: "Tell the user a restore is already in progress on this resource. Wait for it to complete — see https://instanode.dev/app/resources.",
0 commit comments