Skip to content

Commit fe9a7ca

Browse files
Ambient Code Botclaude
andcommitted
fix(runner,rbac,cli,routes): credential auth reliability and SSE improvements
- runner: retry credential fetch with fresh CP token on 401 instead of aborting the turn; adds refresh_bot_token() to utils.py and wires it into _fetch_credential() for both the direct BOT_TOKEN and caller-token fallback paths - rbac: fix pathToAction() so GET /credentials/{id}/token resolves to action "token" (matching credential:token-reader role) instead of "read" - cli: improve acpctl session send -f output with reasoning accumulation ([thinking] blocks), tool call annotations ([ToolName] → result), and proper newline handling for streamed text - routes: add haproxy.router.openshift.io/timeout: 10m to all ambient-api-server Routes (production, mpp-openshift, local-dev) to prevent 504s on long-running SSE streams 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 85fdf9b commit fe9a7ca

7 files changed

Lines changed: 129 additions & 13 deletions

File tree

components/ambient-api-server/pkg/rbac/middleware.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func (m *DBAuthorizationMiddleware) AuthorizeApi(next http.Handler) http.Handler
5757
}
5858

5959
func (m *DBAuthorizationMiddleware) isAllowed(g *gorm.DB, username, method, path string) (bool, error) {
60-
action := httpMethodToAction(method)
60+
action := pathToAction(method, path)
6161
resource := pathToResource(path)
6262

6363
var bindings []roleBindingRow
@@ -130,6 +130,22 @@ func httpMethodToAction(method string) string {
130130
}
131131
}
132132

133+
func pathToAction(method, path string) string {
134+
parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
135+
for i, p := range parts {
136+
if p == "v1" && i+2 < len(parts) {
137+
last := parts[len(parts)-1]
138+
switch last {
139+
case "token":
140+
return "token"
141+
case "start", "stop", "ignite", "ignition":
142+
return last
143+
}
144+
}
145+
}
146+
return httpMethodToAction(method)
147+
}
148+
133149
func pathToResource(path string) string {
134150
parts := strings.Split(strings.TrimPrefix(path, "/"), "/")
135151
for i, p := range parts {

components/ambient-cli/cmd/acpctl/session/send.go

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ func runSend(cmd *cobra.Command, args []string) error {
7878

7979
out := cmd.OutOrStdout()
8080
scanner := bufio.NewScanner(stream)
81+
var reasoningBuf strings.Builder
82+
var inText bool
8183
for scanner.Scan() {
8284
line := scanner.Text()
8385
if !strings.HasPrefix(line, "data: ") {
@@ -91,18 +93,53 @@ func runSend(cmd *cobra.Command, args []string) error {
9193
}
9294

9395
var evt struct {
94-
Type string `json:"type"`
95-
Delta string `json:"delta"`
96+
Type string `json:"type"`
97+
Delta string `json:"delta"`
98+
ToolCallName string `json:"toolCallName"`
99+
Content string `json:"content"`
96100
}
97101
if err := json.Unmarshal([]byte(data), &evt); err != nil {
98102
continue
99103
}
100-
if evt.Type == "TEXT_MESSAGE_CONTENT" && evt.Delta != "" {
101-
fmt.Fprint(out, evt.Delta)
104+
switch evt.Type {
105+
case "REASONING_MESSAGE_CONTENT":
106+
reasoningBuf.WriteString(evt.Delta)
107+
case "REASONING_END":
108+
if reasoningBuf.Len() > 0 {
109+
fmt.Fprintf(out, "[thinking] %s\n", strings.TrimSpace(reasoningBuf.String()))
110+
reasoningBuf.Reset()
111+
}
112+
case "TEXT_MESSAGE_CONTENT":
113+
if evt.Delta != "" {
114+
inText = true
115+
fmt.Fprint(out, evt.Delta)
116+
}
117+
case "TEXT_MESSAGE_END":
118+
if inText {
119+
fmt.Fprintln(out)
120+
inText = false
121+
}
122+
case "TOOL_CALL_START":
123+
if evt.ToolCallName != "" {
124+
fmt.Fprintf(out, "[%s] ", evt.ToolCallName)
125+
}
126+
case "TOOL_CALL_RESULT":
127+
if evt.Content != "" {
128+
var content string
129+
if err := json.Unmarshal([]byte(evt.Content), &content); err != nil {
130+
content = evt.Content
131+
}
132+
lines := strings.SplitN(strings.TrimSpace(content), "\n", 4)
133+
preview := strings.Join(lines, " | ")
134+
if len(lines) >= 4 {
135+
preview += " ..."
136+
}
137+
fmt.Fprintf(out, "→ %s\n", preview)
138+
}
102139
}
103140
}
104141

105-
if !sendFollowJSON {
142+
if inText {
106143
fmt.Fprintln(out)
107144
}
108145

components/manifests/overlays/local-dev/ambient-api-server-route.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ metadata:
66
labels:
77
app: ambient-api-server
88
component: api
9+
annotations:
10+
haproxy.router.openshift.io/timeout: 10m
911
spec:
1012
to:
1113
kind: Service
@@ -23,6 +25,8 @@ metadata:
2325
labels:
2426
app: ambient-api-server
2527
component: grpc
28+
annotations:
29+
haproxy.router.openshift.io/timeout: 10m
2630
spec:
2731
to:
2832
kind: Service

components/manifests/overlays/mpp-openshift/ambient-api-server-route.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ metadata:
77
app: ambient-api-server
88
component: api
99
shard: internal
10+
annotations:
11+
haproxy.router.openshift.io/timeout: 10m
1012
spec:
1113
to:
1214
kind: Service

components/manifests/overlays/production/ambient-api-server-route.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ metadata:
66
labels:
77
app: ambient-api-server
88
component: api
9+
annotations:
10+
haproxy.router.openshift.io/timeout: 10m
911
spec:
1012
to:
1113
kind: Service
@@ -23,6 +25,8 @@ metadata:
2325
labels:
2426
app: ambient-api-server
2527
component: grpc
28+
annotations:
29+
haproxy.router.openshift.io/timeout: 10m
2630
spec:
2731
to:
2832
kind: Service

components/runners/ambient-runner/ambient_runner/platform/auth.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
from urllib.parse import urlparse
1818

1919
from ambient_runner.platform.context import RunnerContext
20-
from ambient_runner.platform.utils import get_bot_token
20+
from ambient_runner.platform.utils import get_bot_token, refresh_bot_token
2121

2222
logger = logging.getLogger(__name__)
2323

@@ -174,6 +174,16 @@ def _do_req():
174174
try:
175175
with _urllib_request.urlopen(fallback_req, timeout=10) as resp:
176176
return resp.read().decode("utf-8", errors="replace")
177+
except _urllib_request.HTTPError as fallback_err:
178+
if fallback_err.code in (401, 403):
179+
return _retry_with_fresh_bot_token()
180+
logger.warning(
181+
f"{credential_type} BOT_TOKEN fallback also failed: {fallback_err}"
182+
)
183+
raise PermissionError(
184+
f"{credential_type} authentication failed: caller token expired "
185+
f"and BOT_TOKEN fallback also failed"
186+
) from fallback_err
177187
except Exception as fallback_err:
178188
logger.warning(
179189
f"{credential_type} BOT_TOKEN fallback also failed: {fallback_err}"
@@ -183,18 +193,40 @@ def _do_req():
183193
f"and BOT_TOKEN fallback also failed"
184194
) from fallback_err
185195
if e.code in (401, 403):
186-
logger.warning(
187-
f"{credential_type} credential fetch failed with HTTP {e.code}: {e}"
188-
)
189-
raise PermissionError(
190-
f"{credential_type} authentication failed with HTTP {e.code}"
191-
) from e
196+
# BOT_TOKEN may have expired — refresh from CP endpoint and retry once.
197+
return _retry_with_fresh_bot_token()
192198
logger.warning(f"{credential_type} credential fetch failed: {e}")
193199
return ""
194200
except Exception as e:
195201
logger.warning(f"{credential_type} credential fetch failed: {e}")
196202
return ""
197203

204+
def _retry_with_fresh_bot_token():
205+
logger.info(
206+
f"{credential_type} got 401 with cached BOT_TOKEN — refreshing from CP endpoint and retrying"
207+
)
208+
try:
209+
fresh_bot = refresh_bot_token()
210+
except Exception as refresh_err:
211+
logger.warning(f"{credential_type} CP token refresh failed: {refresh_err}")
212+
raise PermissionError(
213+
f"{credential_type} authentication failed with HTTP 401"
214+
) from refresh_err
215+
retry_req = _urllib_request.Request(url, method="GET")
216+
if fresh_bot:
217+
retry_req.add_header("Authorization", f"Bearer {fresh_bot}")
218+
if context.current_user_id:
219+
retry_req.add_header("X-Runner-Current-User", context.current_user_id)
220+
try:
221+
with _urllib_request.urlopen(retry_req, timeout=10) as resp:
222+
logger.info(f"{credential_type} retry with fresh BOT_TOKEN succeeded")
223+
return resp.read().decode("utf-8", errors="replace")
224+
except Exception as retry_err:
225+
logger.warning(f"{credential_type} retry with fresh BOT_TOKEN failed: {retry_err}")
226+
raise PermissionError(
227+
f"{credential_type} authentication failed with HTTP 401"
228+
) from retry_err
229+
198230
resp_text = await loop.run_in_executor(None, _do_req)
199231
if not resp_text:
200232
return {}

components/runners/ambient-runner/ambient_runner/platform/utils.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,27 @@ def get_bot_token() -> str:
7171
return (os.getenv("BOT_TOKEN") or "").strip()
7272

7373

74+
def refresh_bot_token() -> str:
75+
"""Fetch a fresh token from the CP token endpoint and update the in-process cache.
76+
77+
Returns the new token, or the current cached token if the CP endpoint is not
78+
configured (local dev mode). Raises RuntimeError if the CP fetch fails.
79+
"""
80+
cp_token_url = os.getenv("AMBIENT_CP_TOKEN_URL", "")
81+
if not cp_token_url:
82+
return get_bot_token()
83+
84+
public_key_pem = os.getenv("AMBIENT_CP_TOKEN_PUBLIC_KEY", "")
85+
session_id = os.getenv("SESSION_ID", "")
86+
if not public_key_pem or not session_id:
87+
logger.warning("refresh_bot_token: CP env vars incomplete, skipping refresh")
88+
return get_bot_token()
89+
90+
from ambient_runner._grpc_client import _fetch_token_from_cp
91+
92+
return _fetch_token_from_cp(cp_token_url, public_key_pem, session_id)
93+
94+
7495
def is_env_truthy(value: str) -> bool:
7596
"""Return True for "1", "true", or "yes" (case-insensitive)."""
7697
return value.strip().lower() in _TRUTHY_VALUES

0 commit comments

Comments
 (0)