feat(tasks): add skill-templated task graph execution

This commit is contained in:
2026-06-23 10:22:58 +08:00
parent 6843d89b2c
commit 53b13e8eac
53 changed files with 4773 additions and 756 deletions

View File

@ -9,6 +9,7 @@ from typing import Any
import httpx
from fastapi import Depends, FastAPI, Header, HTTPException, Query, Request
from fastapi.responses import JSONResponse
from pydantic import ValidationError
from app.json_store import JsonStore
from app.models import (
@ -823,7 +824,10 @@ async def get_internal_channel_settings(backend_id: str, channel_id: str) -> dic
async def oauth_token(request: Request) -> OAuthTokenResponse:
content_type = request.headers.get("content-type", "")
if "application/json" in content_type:
payload = OAuthTokenRequest.model_validate(await request.json())
try:
payload = OAuthTokenRequest.model_validate(await request.json())
except ValidationError as exc:
raise HTTPException(status_code=422, detail=exc.errors()) from exc
else:
form = await request.form()
payload = _parse_token_request_from_form(dict(form))

View File

@ -3,7 +3,7 @@ from __future__ import annotations
from datetime import datetime, timezone
from typing import Any
from pydantic import BaseModel, Field
from pydantic import AliasChoices, BaseModel, Field
def utcnow_iso() -> str:
@ -173,7 +173,7 @@ class OAuthTokenRequest(BaseModel):
grant_type: str = "client_credentials"
client_id: str
client_secret: str
aud: str
aud: str = Field(validation_alias=AliasChoices("aud", "audience"))
scopes: list[str] = Field(default_factory=list)

View File

@ -0,0 +1,61 @@
from __future__ import annotations
import importlib
from fastapi.testclient import TestClient
def _client(tmp_path, monkeypatch) -> TestClient:
monkeypatch.setenv("AUTHZ_DATA_DIR", str(tmp_path))
monkeypatch.setenv("AUTHZ_PRIVATE_KEY_PATH", str(tmp_path / "signing_key.pem"))
monkeypatch.setenv("AUTHZ_INTERNAL_TOKEN", "test-internal-token")
import app.main as main
main = importlib.reload(main)
return TestClient(main.app, raise_server_exceptions=False)
def _register_backend(client: TestClient) -> dict:
response = client.post(
"/backends/register",
json={"backend_id": "alice", "name": "Alice", "base_url": "http://alice.local"},
)
assert response.status_code == 200
return response.json()
def test_json_token_request_accepts_audience_alias(tmp_path, monkeypatch) -> None:
with _client(tmp_path, monkeypatch) as client:
backend = _register_backend(client)
response = client.post(
"/oauth/token",
json={
"grant_type": "client_credentials",
"client_id": backend["client_id"],
"client_secret": backend["client_secret"],
"audience": "mcp:outlook_mcp",
"scopes": ["list_tools", "tool:auth_status"],
},
)
assert response.status_code == 200
body = response.json()
assert body["access_token"]
assert body["token_type"] == "bearer"
def test_json_token_request_validation_errors_return_422(tmp_path, monkeypatch) -> None:
with _client(tmp_path, monkeypatch) as client:
backend = _register_backend(client)
response = client.post(
"/oauth/token",
json={
"grant_type": "client_credentials",
"client_id": backend["client_id"],
"client_secret": backend["client_secret"],
"scopes": ["list_tools"],
},
)
assert response.status_code == 422
assert response.json()["detail"]