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
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.261.342"
VERSION = "0.261.343"
IS_DEVELOPMENT = is_development_env_enabled()

# Opt-out for deployments where App Service Easy Auth is active but the platform
Expand Down
30 changes: 30 additions & 0 deletions application/single_app/functions_orchestration_execution.py
Original file line number Diff line number Diff line change
Expand Up @@ -1853,6 +1853,36 @@
)
return list(self._frames)

def credential_continuation_status(self, item):

Check warning on line 1856 in application/single_app/functions_orchestration_execution.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
"""Project the selected step from our acknowledged durable finalization."""
record = self._final_record
continuation = item.get("continuation") or {}
if record is None or (
record.get("id") != continuation.get("run_id")
or record.get("id") != self.record["id"]
or record.get("user_id") != item.get("user_id")
or record.get("conversation_id") != item.get("conversation_id")

Check warning on line 1864 in application/single_app/functions_orchestration_execution.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
):
raise HarnessExecutionError("result_not_ready")
step = next(
(row for row in record.get("execution_steps") or []
if row["step_id"] == continuation.get("step_id")),
None,
)
if step is not None:
if step["status"] in {"completed", "partial"}:
return "completed"
if step["status"] in {"failed", "cancelled"}:
return "failed"
wait = step.get("wait") or {}
if step["status"] == "waiting" and wait.get("kind") == "action_credentials" and (

Check warning on line 1878 in application/single_app/functions_orchestration_execution.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
wait.get("request_id") and wait["request_id"] != item["id"]
):
return "superseded"
if record.get("status") in {"failed", "cancelled"}:
return "failed"
raise HarnessExecutionError("result_not_ready")

def _preparation_error(self, error):
_log_failure(
"Headless execution preparation failed.", self.record, error,
Expand Down
26 changes: 24 additions & 2 deletions application/single_app/functions_orchestration_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1237,15 +1237,37 @@
if type(wait) is not dict:
raise ResultContractError('result_wait_required')
if wait.get("kind") == "action_credentials":
from functions_action_credentials import authorize_request, credential_continuation
from functions_action_credentials import authorize_request

Check warning on line 1240 in application/single_app/functions_orchestration_executor.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
from functions_orchestration_adapters import _credential_step_continuation

if (
set(wait) != {"kind", "request_id"} or user_id != context.user_id
or step.get("capability_id") not in {"action_invoke", "agent_invoke"}
):
raise ResultContractError("result_wait_invalid")
request, _manifest = authorize_request(user_id, wait["request_id"])
try:

Check warning on line 1248 in application/single_app/functions_orchestration_executor.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
identity = context.agent_execution_identity

Check warning on line 1249 in application/single_app/functions_orchestration_executor.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
if (
identity is None or identity.user_id != user_id
or identity.conversation_id != context.conversation_id or not callable(identity.bridge)

Check warning on line 1252 in application/single_app/functions_orchestration_executor.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
):
raise PermissionError("The credential execution identity is unavailable.")

Check warning on line 1254 in application/single_app/functions_orchestration_executor.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.

Check warning on line 1254 in application/single_app/functions_orchestration_executor.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
# Catalog authorization uses legacy request helpers, just like action invocation.

Check warning on line 1255 in application/single_app/functions_orchestration_executor.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
with identity.bridge({}):
request, _manifest = authorize_request(user_id, wait["request_id"])
except Exception as error:
log_event(
f"{_LOG_PREFIX} Credential resume authorization failed.",
level=logging.WARNING,
extra={
**workflow_log_context(
run_id=context.run_id, conversation_id=context.conversation_id,
step_id=step["step_id"],
),
"stage": "credential_resume_authorization", "error_type": type(error).__name__,
},
)
raise
descriptor = _credential_step_continuation(step, context)
if any(
request["continuation"].get(key) != descriptor.get(key) for key in ("run_id", "step_id", "checkpoint_id")
Expand Down
21 changes: 15 additions & 6 deletions application/single_app/route_backend_action_credentials.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from functools import wraps
import hmac
import logging
import secrets
import uuid
from datetime import datetime, timezone
Expand All @@ -11,7 +12,7 @@

from action_credential_contracts import ActionCredentialError, ActionCredentialsRequired
from functions_authentication import get_current_user_id, login_required, user_required
from functions_appinsights import log_event
from functions_appinsights import log_event, workflow_log_context
from functions_action_credentials import (
authorize_conversation, authorize_request, credential_continuation, safe_request, submit_credentials,
)
Expand Down Expand Up @@ -77,11 +78,19 @@ def __init__(self, execution, item, user_id):
def execute(self, **kwargs):
try:
yield from self.execution.execute(**kwargs)
except Exception:
_finish_request(self.user_id, self.item["id"], self.item["continuation_claim"], "failed")
raise
else:
_finish_request(self.user_id, self.item["id"], self.item["continuation_claim"], "completed")
finally:
try:
status = self.execution.credential_continuation_status(self.item)
_finish_request(self.user_id, self.item["id"], self.item["continuation_claim"], status)
except Exception as error:
log_event("[PLUGINS] Credential continuation outcome could not be confirmed.", level=logging.WARNING, extra={
**workflow_log_context(
run_id=self.item["continuation"]["run_id"], conversation_id=self.item["conversation_id"],
step_id=self.item["continuation"]["step_id"],
),
"stage": "credential_continuation_finalization", "error_type": type(error).__name__,
})
raise

def close(self):
return self.execution.close()
Expand Down
89 changes: 76 additions & 13 deletions application/v2_ui/src/components/chat/CredentialRequestCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import { useBootstrapStore } from '../../stores/bootstrapStore';
import { continueCredentialChat } from '../../stores/chatStore';
import { continueCredentialOrchestration } from '../../lib/orchestrationController';
import { ACTION_CREDENTIAL_EVENT, readCredentialRequest } from '../../lib/actionCredentialEvents';
import { cancelActionCredentials, clearCredentialSession, fetchCredentialRequests, saveActionCredentials } from '../../lib/actionCredentials';
import { cancelActionCredentials, clearCredentialSession, fetchActionCredentialRequest, fetchCredentialRequests, saveActionCredentials } from '../../lib/actionCredentials';
import { fetchOrchestrationRun, isOrchestrationRunWaiting } from '../../lib/orchestration';
import type { CredentialRequest, CredentialSlot, CredentialSubmission } from '../../lib/actionCredentialTypes';
import { ApiError } from '../../lib/apiClient';

Expand Down Expand Up @@ -98,6 +99,9 @@ export function CredentialRequestCard({ credentialRequest, onUpdate, onRemove }:
const [busy, setBusy] = useState(false);
const [error, setError] = useState('');
const [saved, setSaved] = useState<CredentialRequest | null>(null);
const [needsRefresh, setNeedsRefresh] = useState(false);
const [closed, setClosed] = useState(false);
const working = useRef(false);
useEffect(() => () => {
controller.current?.abort();
formRef.current?.reset();
Expand All @@ -107,13 +111,15 @@ export function CredentialRequestCard({ credentialRequest, onUpdate, onRemove }:
const continueWork = async (request: CredentialRequest) => {
if (request.run_id) await continueCredentialOrchestration(request);
else await continueCredentialChat(request);
onRemove();
if (!controller.current?.signal.aborted) onRemove();
};
const submit = async () => {
if (busy) return;
if (working.current || needsRefresh || closed) return;
working.current = true;
setBusy(true);
setError('');
controller.current = new AbortController();
let confirmedSave = ready;
try {
if (ready) {
await continueWork(ready);
Expand Down Expand Up @@ -144,20 +150,70 @@ export function CredentialRequestCard({ credentialRequest, onUpdate, onRemove }:
} finally {
slots.length = 0;
}
if (controller.current.signal.aborted) return;
setSaved(updated);
confirmedSave = updated;
await continueWork(updated);
} catch {
if (!controller.current?.signal.aborted) {
setError(ready || saved
? 'Credentials are saved, but the action could not continue. Reload requests to check its status.'
: 'Saving or continuing failed. Reload requests to check whether credentials were saved before entering them again.');
setNeedsRefresh(true);
setError(confirmedSave
? 'Your credentials are saved, but continuation could not be confirmed. Refresh the saved request before trying again.'
: 'Saving could not be confirmed. Refresh the saved request before entering credentials again.');
}
} finally {
working.current = false;
if (!controller.current?.signal.aborted) setBusy(false);
}
};
const refreshSavedRequest = async () => {
if (working.current) return;
working.current = true;
setBusy(true);
controller.current = new AbortController();
try {
const current = await fetchActionCredentialRequest(saved || credentialRequest, controller.current.signal);
let waiting = true;
if (['pending', 'ready'].includes(current.status) && current.run_id) {
const run = await fetchOrchestrationRun(current.run_id, {
conversationId: current.conversation_id, signal: controller.current.signal,
});
if (!run || run.run_id !== current.run_id || run.conversation_id !== current.conversation_id ||
(current.turn_id && current.turn_id !== run.turn_id)) {
throw new Error('The saved work could not be confirmed.');
}
waiting = isOrchestrationRunWaiting(run);
}
if (controller.current.signal.aborted) return;
if (current.status === 'pending' && waiting) {
setSaved(null);
setClosed(false);
setNeedsRefresh(false);
setError('');
onUpdate(current);
} else {
setSaved(current.status === 'ready' || ready ? current : null);
const canContinue = current.status === 'ready' && waiting;
setClosed(!canContinue);
setNeedsRefresh(!canContinue);
setError(canContinue ? ''
: current.run_id
? 'This work is no longer waiting for these credentials. Review run details for saved progress and recovery options.'
: 'This work is no longer waiting for these credentials. Review the conversation before starting new work.');
}
} catch {
if (!controller.current.signal.aborted) {
setNeedsRefresh(true);
setError('The saved request could not be refreshed. Reload this conversation before trying again.');
}
} finally {
working.current = false;
if (!controller.current.signal.aborted) setBusy(false);
}
};
const cancel = async () => {
if (busy) return;
if (working.current || needsRefresh || closed) return;
working.current = true;
setBusy(true);
setError('');
formRef.current?.reset();
Expand All @@ -168,6 +224,7 @@ export function CredentialRequestCard({ credentialRequest, onUpdate, onRemove }:
} catch {
if (!controller.current.signal.aborted) setError('The credential request could not be cancelled. Reload requests before trying again.');
} finally {
working.current = false;
if (!controller.current.signal.aborted) setBusy(false);
}
};
Expand All @@ -186,7 +243,7 @@ export function CredentialRequestCard({ credentialRequest, onUpdate, onRemove }:
<form ref={formRef} className="mt-4 space-y-4" onSubmit={(event) => { event.preventDefault(); void submit(); }}
aria-busy={busy} autoComplete="off">
<fieldset disabled={busy} className="space-y-4">
{!ready ? credentialRequest.requirements.map((requirement) => {
{!ready && !needsRefresh ? credentialRequest.requirements.map((requirement) => {
const prefix = `${id}-${requirement.slot}`;
const candidates = credentialRequest.identities.filter((item) => item.auth_type === requirement.auth_type);
const selectedIdentity = candidates.find((item) => item.id === identities[requirement.slot]);
Expand Down Expand Up @@ -230,15 +287,21 @@ export function CredentialRequestCard({ credentialRequest, onUpdate, onRemove }:
required autoComplete="new-password" spellCheck={false} />
</> : null}
</fieldset>;
}) : <p role="status" className="text-sm text-ok">Your personal identities are saved. Continue the blocked action when ready.</p>}
}) : ready ? <p role="status" className="text-sm text-ok">
{needsRefresh || closed ? 'Your personal identities are saved.'
: 'Your personal identities are saved. Continue the blocked action when ready.'}
</p> : null}
{error ? <p role="alert" className="text-sm text-danger">{error}</p> : null}
<div className="flex flex-wrap items-center gap-2">
<GlassButton type="submit" variant="primary">{busy ? 'Working...' : ready ? 'Continue' : 'Save and continue'}</GlassButton>
<GlassButton type="button" onClick={() => void cancel()}>Cancel blocked work</GlassButton>
{!closed ? <>
<GlassButton type="submit" variant="primary" disabled={needsRefresh}>{busy ? 'Working...' : ready ? 'Continue' : 'Save and continue'}</GlassButton>
<GlassButton type="button" disabled={needsRefresh} onClick={() => void cancel()}>Cancel blocked work</GlassButton>
</> : null}
{needsRefresh || saved ? <GlassButton type="button" onClick={() => void refreshSavedRequest()}>
Refresh saved request
</GlassButton> : null}
</div>
</fieldset>
</form>
{saved && saved.etag !== credentialRequest.etag && !busy
? <GlassButton size="sm" onClick={() => onUpdate(saved)}>Refresh saved request</GlassButton> : null}
</section>;
}
28 changes: 18 additions & 10 deletions application/v2_ui/src/lib/actionCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,29 @@ function captureCsrf(result: { csrf_token?: unknown }, epoch: number): void {

async function credentialHeaders(credentialRequest: CredentialRequest, signal?: AbortSignal): Promise<Record<string, string>> {
if (!csrfToken) {
const epoch = sessionEpoch;
const result = await request<{ credential_request: unknown; csrf_token?: unknown }>(
`/api/action-credentials/${encodeURIComponent(credentialRequest.id)}`,
{ cache: 'no-store', signal },
);
const current = readCredentialRequest(result.credential_request);
if (current.id !== credentialRequest.id || current.conversation_id !== credentialRequest.conversation_id) {
throw new Error('The credential request no longer belongs to this conversation.');
}
captureCsrf(result, epoch);
await fetchActionCredentialRequest(credentialRequest, signal);
}
if (!csrfToken) throw new Error('The credential request could not be authorized. Reload this conversation.');
return { 'X-CSRFToken': csrfToken };
}

export async function fetchActionCredentialRequest(
credentialRequest: CredentialRequest, signal?: AbortSignal,
): Promise<CredentialRequest> {
const epoch = sessionEpoch;
const result = await request<{ credential_request: unknown; csrf_token?: unknown }>(
`/api/action-credentials/${encodeURIComponent(credentialRequest.id)}`,
{ cache: 'no-store', signal },
);
const current = readCredentialRequest(result.credential_request);
if (current.id !== credentialRequest.id || current.conversation_id !== credentialRequest.conversation_id ||
current.run_id !== credentialRequest.run_id || current.turn_id !== credentialRequest.turn_id) {
throw new Error('The credential request no longer belongs to this work.');
}
captureCsrf(result, epoch);
return current;
}

export async function fetchCredentialRequests(conversationId: string, signal?: AbortSignal): Promise<CredentialRequest[]> {
const epoch = sessionEpoch;
const result = await request<{ requests: unknown[]; csrf_token?: unknown }>(
Expand Down
Loading
Loading