@slashfi/agents-sdk 0.11.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-definitions/auth.d.ts +17 -1
- package/dist/agent-definitions/auth.d.ts.map +1 -1
- package/dist/agent-definitions/auth.js +123 -4
- package/dist/agent-definitions/auth.js.map +1 -1
- package/dist/agent-definitions/integrations.d.ts +2 -14
- package/dist/agent-definitions/integrations.d.ts.map +1 -1
- package/dist/agent-definitions/integrations.js +64 -19
- package/dist/agent-definitions/integrations.js.map +1 -1
- package/dist/agent-definitions/remote-registry.d.ts +19 -14
- package/dist/agent-definitions/remote-registry.d.ts.map +1 -1
- package/dist/agent-definitions/remote-registry.js +219 -381
- package/dist/agent-definitions/remote-registry.js.map +1 -1
- package/dist/agent-definitions/users.d.ts.map +1 -1
- package/dist/agent-definitions/users.js +29 -1
- package/dist/agent-definitions/users.js.map +1 -1
- package/dist/define.d.ts +6 -4
- package/dist/define.d.ts.map +1 -1
- package/dist/define.js +82 -3
- package/dist/define.js.map +1 -1
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/jwt.js +1 -1
- package/dist/jwt.js.map +1 -1
- package/dist/key-manager.d.ts +76 -0
- package/dist/key-manager.d.ts.map +1 -0
- package/dist/key-manager.js +156 -0
- package/dist/key-manager.js.map +1 -0
- package/dist/server.d.ts +39 -0
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +228 -52
- package/dist/server.js.map +1 -1
- package/dist/types.d.ts +53 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/agent-definitions/auth.ts +165 -6
- package/src/agent-definitions/integrations.ts +59 -28
- package/src/agent-definitions/remote-registry.ts +219 -513
- package/src/agent-definitions/users.ts +35 -1
- package/src/define.ts +98 -6
- package/src/index.ts +3 -1
- package/src/jwt.ts +1 -1
- package/src/key-manager.test.ts +273 -0
- package/src/key-manager.ts +257 -0
- package/src/server.test.ts +284 -0
- package/src/server.ts +334 -60
- package/src/types.ts +44 -1
package/src/server.ts
CHANGED
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
} from "./agent-definitions/secrets.js";
|
|
33
33
|
import { verifyJwt } from "./jwt.js";
|
|
34
34
|
import type { SigningKey } from "./jwt.js";
|
|
35
|
-
import { generateSigningKey, importSigningKey, exportSigningKey, buildJwks, verifyJwtLocal, verifyJwtFromIssuer } from "./jwt.js";
|
|
35
|
+
import { generateSigningKey, importSigningKey, exportSigningKey, buildJwks, verifyJwtLocal, verifyJwtFromIssuer, signJwtES256 } from "./jwt.js";
|
|
36
36
|
import type { AgentRegistry } from "./registry.js";
|
|
37
37
|
import type { AgentDefinition, CallAgentRequest, Visibility } from "./types.js";
|
|
38
38
|
|
|
@@ -48,6 +48,37 @@ export interface TrustedIssuer {
|
|
|
48
48
|
scopes: string[];
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
/** OAuth identity provider for /oauth/authorize + /oauth/callback flows */
|
|
54
|
+
export interface OAuthIdentityProvider {
|
|
55
|
+
/**
|
|
56
|
+
* Handle /oauth/authorize — redirect the user to an external IdP.
|
|
57
|
+
* Return a Response (typically a 302 redirect).
|
|
58
|
+
*/
|
|
59
|
+
authorize(req: Request, params: {
|
|
60
|
+
/** The verified JWT from the foreign registry */
|
|
61
|
+
token: string;
|
|
62
|
+
/** Claims from the verified JWT */
|
|
63
|
+
claims: Record<string, unknown>;
|
|
64
|
+
/** Where to redirect after completion */
|
|
65
|
+
redirectUri: string;
|
|
66
|
+
/** Base URL of this server */
|
|
67
|
+
baseUrl: string;
|
|
68
|
+
/** OAuth scope (e.g. "setup" for tenant creation flow) */
|
|
69
|
+
scope?: string;
|
|
70
|
+
}): Promise<Response>;
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Handle /oauth/callback — process the IdP response.
|
|
74
|
+
* Should link the foreign identity to a local user and redirect back.
|
|
75
|
+
* Return a Response (typically a 302 redirect to redirectUri).
|
|
76
|
+
*/
|
|
77
|
+
callback(req: Request, params: {
|
|
78
|
+
/** Base URL of this server */
|
|
79
|
+
baseUrl: string;
|
|
80
|
+
}): Promise<Response>;
|
|
81
|
+
}
|
|
51
82
|
export interface AgentServerOptions {
|
|
52
83
|
/** Port to listen on (default: 3000) */
|
|
53
84
|
port?: number;
|
|
@@ -67,9 +98,15 @@ export interface AgentServerOptions {
|
|
|
67
98
|
trustedIssuers?: (TrustedIssuer | string)[];
|
|
68
99
|
/** Pre-generated signing key (if not provided, one is generated on start) */
|
|
69
100
|
signingKey?: SigningKey;
|
|
101
|
+
/** OAuth identity provider for cross-registry user linking */
|
|
102
|
+
oauthIdentityProvider?: OAuthIdentityProvider;
|
|
103
|
+
/** Key store for managed key rotation (if provided, uses createKeyManager instead of simple key gen) */
|
|
104
|
+
keyStore?: import("./key-manager.js").KeyStore;
|
|
70
105
|
}
|
|
71
106
|
|
|
72
107
|
export interface AgentServer {
|
|
108
|
+
/** Initialize signing keys without starting HTTP server */
|
|
109
|
+
initKeys(): Promise<void>;
|
|
73
110
|
/** Start the server */
|
|
74
111
|
start(): Promise<void>;
|
|
75
112
|
/** Stop the server */
|
|
@@ -80,6 +117,10 @@ export interface AgentServer {
|
|
|
80
117
|
url: string | null;
|
|
81
118
|
/** The agent registry this server uses */
|
|
82
119
|
registry: AgentRegistry;
|
|
120
|
+
/** Sign a JWT with the server's signing key (for outbound calls) */
|
|
121
|
+
signJwt(claims: Record<string, unknown>): Promise<string>;
|
|
122
|
+
/** Dynamically add a trusted JWT issuer at runtime */
|
|
123
|
+
addTrustedIssuer(issuerUrl: string, scopes?: string[]): void;
|
|
83
124
|
}
|
|
84
125
|
|
|
85
126
|
// ============================================
|
|
@@ -111,6 +152,7 @@ export interface AuthConfig {
|
|
|
111
152
|
}
|
|
112
153
|
|
|
113
154
|
export interface ResolvedAuth {
|
|
155
|
+
issuer?: string;
|
|
114
156
|
callerId: string;
|
|
115
157
|
callerType: "agent" | "user" | "system";
|
|
116
158
|
scopes: string[];
|
|
@@ -401,6 +443,7 @@ export function createAgentServer(
|
|
|
401
443
|
serverName = "agents-sdk",
|
|
402
444
|
serverVersion = "1.0.0",
|
|
403
445
|
secretStore,
|
|
446
|
+
oauthIdentityProvider,
|
|
404
447
|
} = options;
|
|
405
448
|
|
|
406
449
|
// Signing keys for JWKS-based auth
|
|
@@ -487,6 +530,7 @@ export function createAgentServer(
|
|
|
487
530
|
if (!req.metadata) req.metadata = {};
|
|
488
531
|
req.metadata.scopes = auth.scopes;
|
|
489
532
|
req.metadata.isRoot = auth.isRoot;
|
|
533
|
+
if (auth.issuer) req.metadata.issuer = auth.issuer;
|
|
490
534
|
}
|
|
491
535
|
if (auth?.isRoot) {
|
|
492
536
|
req.callerType = "system";
|
|
@@ -546,79 +590,207 @@ export function createAgentServer(
|
|
|
546
590
|
// OAuth2 token handler
|
|
547
591
|
// ──────────────────────────────────────────
|
|
548
592
|
|
|
593
|
+
// Resolve public-facing base URL, respecting reverse proxy headers
|
|
594
|
+
const resolveBaseUrl = (r: Request): string => {
|
|
595
|
+
const fwdProto = r.headers.get("x-forwarded-proto");
|
|
596
|
+
const fwdHost = r.headers.get("x-forwarded-host");
|
|
597
|
+
if (fwdProto && fwdHost) return `${fwdProto}://${fwdHost}`;
|
|
598
|
+
return new URL(r.url).origin;
|
|
599
|
+
};
|
|
600
|
+
|
|
549
601
|
async function handleOAuthToken(req: Request): Promise<Response> {
|
|
550
602
|
if (!authConfig) {
|
|
551
603
|
return jsonResponse({ error: "auth_not_configured" }, 404);
|
|
552
604
|
}
|
|
553
605
|
|
|
554
606
|
const contentType = req.headers.get("Content-Type") ?? "";
|
|
555
|
-
let
|
|
556
|
-
let clientId: string;
|
|
557
|
-
let clientSecret: string;
|
|
607
|
+
let params: Record<string, string>;
|
|
558
608
|
|
|
559
609
|
if (contentType.includes("application/x-www-form-urlencoded")) {
|
|
560
610
|
const body = await req.text();
|
|
561
|
-
const
|
|
562
|
-
|
|
563
|
-
clientId = params.get("client_id") ?? "";
|
|
564
|
-
clientSecret = params.get("client_secret") ?? "";
|
|
611
|
+
const urlParams = new URLSearchParams(body);
|
|
612
|
+
params = Object.fromEntries(urlParams.entries());
|
|
565
613
|
} else {
|
|
566
|
-
|
|
567
|
-
grantType = body.grant_type ?? "";
|
|
568
|
-
clientId = body.client_id ?? "";
|
|
569
|
-
clientSecret = body.client_secret ?? "";
|
|
614
|
+
params = (await req.json()) as Record<string, string>;
|
|
570
615
|
}
|
|
571
616
|
|
|
572
|
-
|
|
573
|
-
return jsonResponse(
|
|
574
|
-
{
|
|
575
|
-
error: "unsupported_grant_type",
|
|
576
|
-
error_description: "Only client_credentials is supported",
|
|
577
|
-
},
|
|
578
|
-
400,
|
|
579
|
-
);
|
|
580
|
-
}
|
|
617
|
+
const grantType = params.grant_type ?? "";
|
|
581
618
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
619
|
+
// ── jwt_exchange grant: verify foreign JWT, resolve local identity ──
|
|
620
|
+
if (grantType === "jwt_exchange") {
|
|
621
|
+
const assertion = params.assertion ?? "";
|
|
622
|
+
if (!assertion) {
|
|
623
|
+
return jsonResponse(
|
|
624
|
+
{ error: "invalid_request", error_description: "Missing assertion parameter" },
|
|
625
|
+
400,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
try {
|
|
630
|
+
const result = await registry.call({
|
|
631
|
+
action: "execute_tool",
|
|
632
|
+
path: "@auth",
|
|
633
|
+
tool: "exchange_token",
|
|
634
|
+
params: { token: assertion },
|
|
635
|
+
callerType: "system",
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
const exchangeResult = (result as any)?.result;
|
|
639
|
+
// If the tool call failed, forward the error
|
|
640
|
+
if ((result as any)?.success === false) {
|
|
641
|
+
return jsonResponse(
|
|
642
|
+
{ error: "server_error", error_description: (result as any)?.error ?? "Exchange tool failed", raw: JSON.stringify(result)?.slice(0, 300) },
|
|
643
|
+
500,
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// ── Reverse registration: if caller is an agent-registry, auto-store connection ──
|
|
648
|
+
try {
|
|
649
|
+
const assertionParts = assertion.split(".");
|
|
650
|
+
if (assertionParts.length === 3) {
|
|
651
|
+
const assertionPayload = JSON.parse(atob(assertionParts[1].replace(/-/g, "+").replace(/_/g, "/"))) as any;
|
|
652
|
+
if (assertionPayload.type === "agent-registry" && assertionPayload.iss && false /* disabled: causes infinite loop */) {
|
|
653
|
+
// Find or create @remote-registry agent and store the reverse connection
|
|
654
|
+
const rrAgent = registry.get("@remote-registry") ?? registry.get("/agents/@remote-registry");
|
|
655
|
+
if (rrAgent) {
|
|
656
|
+
const setupTool = (rrAgent as any).tools?.find((t: any) => t.name === "setup_integration");
|
|
657
|
+
if (setupTool?.execute) {
|
|
658
|
+
try {
|
|
659
|
+
await setupTool.execute(
|
|
660
|
+
{ url: assertionPayload.iss, name: assertionPayload.name ?? "remote-registry" },
|
|
661
|
+
{ callerId: "system", callerType: "system", tenantId: "default", agentPath: "@remote-registry" },
|
|
662
|
+
);
|
|
663
|
+
console.error(`[jwt_exchange] Reverse connection stored for ${assertionPayload.iss}`);
|
|
664
|
+
} catch (setupErr) {
|
|
665
|
+
console.error(`[jwt_exchange] Reverse registration setup failed:`, setupErr);
|
|
666
|
+
}
|
|
667
|
+
} else {
|
|
668
|
+
console.error("[jwt_exchange] @remote-registry has no setup_integration tool — reverse registration skipped");
|
|
669
|
+
}
|
|
670
|
+
} else {
|
|
671
|
+
console.error("[jwt_exchange] @remote-registry agent not found — reverse registration skipped");
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} catch (reverseErr) {
|
|
676
|
+
console.error("[jwt_exchange] Reverse registration check failed:", reverseErr);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
if (!exchangeResult) {
|
|
680
|
+
return jsonResponse(
|
|
681
|
+
{ error: "server_error", error_description: `Exchange returned null: ${JSON.stringify(result)?.slice(0, 300)}` },
|
|
682
|
+
500,
|
|
683
|
+
);
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
// User not linked yet — needs OAuth identity linking
|
|
687
|
+
if (exchangeResult.needsAuth) {
|
|
688
|
+
const baseUrl = resolveBaseUrl(req);
|
|
689
|
+
const authorizeUrl = new URL(`${baseUrl}${basePath}/oauth/authorize`);
|
|
690
|
+
authorizeUrl.searchParams.set("token", assertion);
|
|
691
|
+
if (params.redirect_uri) {
|
|
692
|
+
authorizeUrl.searchParams.set("redirect_uri", params.redirect_uri);
|
|
693
|
+
}
|
|
694
|
+
if (params.scope) {
|
|
695
|
+
authorizeUrl.searchParams.set("scope", params.scope);
|
|
696
|
+
}
|
|
697
|
+
return jsonResponse(
|
|
698
|
+
{
|
|
699
|
+
error: "identity_required",
|
|
700
|
+
error_description: "User identity not linked. Redirect to authorize_url to complete linking.",
|
|
701
|
+
authorize_url: authorizeUrl.toString(),
|
|
702
|
+
tenant_id: exchangeResult.tenantId,
|
|
703
|
+
},
|
|
704
|
+
403,
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// User found — sign a local access token
|
|
709
|
+
if (exchangeResult.userId && serverSigningKeys.length > 0) {
|
|
710
|
+
const sigKey = serverSigningKeys[0];
|
|
711
|
+
const token = await signJwtES256(
|
|
712
|
+
{
|
|
713
|
+
sub: exchangeResult.userId,
|
|
714
|
+
name: exchangeResult.userId,
|
|
715
|
+
scopes: ["*"],
|
|
716
|
+
tenantId: exchangeResult.tenantId,
|
|
717
|
+
},
|
|
718
|
+
sigKey.privateKey,
|
|
719
|
+
sigKey.kid,
|
|
720
|
+
resolveBaseUrl(req),
|
|
721
|
+
`${authConfig.tokenTtl ?? 3600}s`,
|
|
722
|
+
);
|
|
723
|
+
|
|
724
|
+
return jsonResponse({
|
|
725
|
+
access_token: token,
|
|
726
|
+
token_type: "Bearer",
|
|
727
|
+
expires_in: authConfig.tokenTtl ?? 3600,
|
|
728
|
+
user_id: exchangeResult.userId,
|
|
729
|
+
tenant_id: exchangeResult.tenantId,
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
return jsonResponse(exchangeResult);
|
|
734
|
+
} catch (err) {
|
|
735
|
+
console.error("[oauth] JWT exchange error:", err);
|
|
736
|
+
return jsonResponse(
|
|
737
|
+
{ error: "server_error", error_description: `JWT exchange failed: ${err instanceof Error ? err.message : String(err)}` },
|
|
738
|
+
500,
|
|
739
|
+
);
|
|
740
|
+
}
|
|
590
741
|
}
|
|
591
742
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
tool: "token",
|
|
597
|
-
params: { clientId, clientSecret },
|
|
598
|
-
callerType: "system",
|
|
599
|
-
});
|
|
743
|
+
// ── client_credentials grant ──
|
|
744
|
+
if (grantType === "client_credentials") {
|
|
745
|
+
const clientId = params.client_id ?? "";
|
|
746
|
+
const clientSecret = params.client_secret ?? "";
|
|
600
747
|
|
|
601
|
-
|
|
602
|
-
if (!tokenResult?.accessToken) {
|
|
748
|
+
if (!clientId || !clientSecret) {
|
|
603
749
|
return jsonResponse(
|
|
604
|
-
{ error: "
|
|
605
|
-
|
|
750
|
+
{ error: "invalid_request", error_description: "Missing client_id or client_secret" },
|
|
751
|
+
400,
|
|
606
752
|
);
|
|
607
753
|
}
|
|
608
754
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
755
|
+
try {
|
|
756
|
+
const result = await registry.call({
|
|
757
|
+
action: "execute_tool",
|
|
758
|
+
path: "@auth",
|
|
759
|
+
tool: "token",
|
|
760
|
+
params: { clientId, clientSecret },
|
|
761
|
+
callerType: "system",
|
|
762
|
+
});
|
|
763
|
+
|
|
764
|
+
const tokenResult = (result as any)?.result;
|
|
765
|
+
if (!tokenResult?.accessToken) {
|
|
766
|
+
return jsonResponse(
|
|
767
|
+
{ error: "invalid_client", error_description: "Authentication failed" },
|
|
768
|
+
401,
|
|
769
|
+
);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return jsonResponse({
|
|
773
|
+
access_token: tokenResult.accessToken,
|
|
774
|
+
token_type: "Bearer",
|
|
775
|
+
expires_in: tokenResult.expiresIn ?? authConfig.tokenTtl,
|
|
776
|
+
refresh_token: tokenResult.refreshToken,
|
|
777
|
+
});
|
|
778
|
+
} catch (err) {
|
|
779
|
+
console.error("[oauth] Token error:", err);
|
|
780
|
+
return jsonResponse(
|
|
781
|
+
{ error: "server_error", error_description: "Token exchange failed" },
|
|
782
|
+
500,
|
|
783
|
+
);
|
|
784
|
+
}
|
|
621
785
|
}
|
|
786
|
+
|
|
787
|
+
return jsonResponse(
|
|
788
|
+
{
|
|
789
|
+
error: "unsupported_grant_type",
|
|
790
|
+
error_description: "Supported grant types: client_credentials, jwt_exchange",
|
|
791
|
+
},
|
|
792
|
+
400,
|
|
793
|
+
);
|
|
622
794
|
}
|
|
623
795
|
|
|
624
796
|
// ──────────────────────────────────────────
|
|
@@ -627,6 +799,7 @@ export function createAgentServer(
|
|
|
627
799
|
|
|
628
800
|
async function fetch(req: Request): Promise<Response> {
|
|
629
801
|
try {
|
|
802
|
+
|
|
630
803
|
const url = new URL(req.url);
|
|
631
804
|
const path = url.pathname.replace(basePath, "") || "/";
|
|
632
805
|
|
|
@@ -667,12 +840,85 @@ export function createAgentServer(
|
|
|
667
840
|
return cors ? addCors(jsonResponse(result)) : jsonResponse(result);
|
|
668
841
|
}
|
|
669
842
|
|
|
670
|
-
// ── POST /oauth/token → OAuth2
|
|
843
|
+
// ── POST /oauth/token → OAuth2 token exchange ──
|
|
671
844
|
if (path === "/oauth/token" && req.method === "POST") {
|
|
672
845
|
const res = await handleOAuthToken(req);
|
|
673
846
|
return cors ? addCors(res) : res;
|
|
674
847
|
}
|
|
675
848
|
|
|
849
|
+
// ── GET /oauth/authorize → Identity linking redirect (browser flow) ──
|
|
850
|
+
if (path === "/oauth/authorize" && req.method === "GET") {
|
|
851
|
+
if (!oauthIdentityProvider) {
|
|
852
|
+
const res = jsonResponse(
|
|
853
|
+
{ error: "not_configured", error_description: "No OAuth identity provider configured" },
|
|
854
|
+
404,
|
|
855
|
+
);
|
|
856
|
+
return cors ? addCors(res) : res;
|
|
857
|
+
}
|
|
858
|
+
const url = new URL(req.url);
|
|
859
|
+
const token = url.searchParams.get("token") ?? "";
|
|
860
|
+
const redirectUri = url.searchParams.get("redirect_uri") ?? "";
|
|
861
|
+
|
|
862
|
+
if (!token) {
|
|
863
|
+
const res = jsonResponse(
|
|
864
|
+
{ error: "invalid_request", error_description: "Missing token parameter" },
|
|
865
|
+
400,
|
|
866
|
+
);
|
|
867
|
+
return cors ? addCors(res) : res;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Verify the JWT against trusted issuers (from store, falling back to config)
|
|
871
|
+
let claims: Record<string, unknown> | null = null;
|
|
872
|
+
const storeIssuers = authConfig?.store?.listTrustedIssuers
|
|
873
|
+
? await authConfig.store.listTrustedIssuers()
|
|
874
|
+
: [];
|
|
875
|
+
const configIssuerUrls = configTrustedIssuers.map(i => typeof i === "string" ? i : i.issuer);
|
|
876
|
+
const allIssuerUrls = [...new Set([...storeIssuers, ...configIssuerUrls])];
|
|
877
|
+
for (const issuerUrl of allIssuerUrls) {
|
|
878
|
+
try {
|
|
879
|
+
const result = await verifyJwtFromIssuer(token, issuerUrl);
|
|
880
|
+
if (result) {
|
|
881
|
+
claims = result as unknown as Record<string, unknown>;
|
|
882
|
+
break;
|
|
883
|
+
}
|
|
884
|
+
} catch { /* try next issuer */ }
|
|
885
|
+
}
|
|
886
|
+
if (!claims) {
|
|
887
|
+
const res = jsonResponse(
|
|
888
|
+
{ error: "invalid_token", error_description: "JWT verification failed against all trusted issuers" },
|
|
889
|
+
401,
|
|
890
|
+
);
|
|
891
|
+
return cors ? addCors(res) : res;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
const baseUrl = resolveBaseUrl(req);
|
|
895
|
+
const scope = url.searchParams.get("scope") ?? undefined;
|
|
896
|
+
const res = await oauthIdentityProvider.authorize(req, {
|
|
897
|
+
token,
|
|
898
|
+
claims,
|
|
899
|
+
redirectUri,
|
|
900
|
+
baseUrl: baseUrl + basePath,
|
|
901
|
+
scope,
|
|
902
|
+
});
|
|
903
|
+
return cors ? addCors(res) : res;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
// ── GET /oauth/callback → Identity linking callback ──
|
|
907
|
+
if (path === "/oauth/callback" && req.method === "GET") {
|
|
908
|
+
if (!oauthIdentityProvider) {
|
|
909
|
+
const res = jsonResponse(
|
|
910
|
+
{ error: "not_configured", error_description: "No OAuth identity provider configured" },
|
|
911
|
+
404,
|
|
912
|
+
);
|
|
913
|
+
return cors ? addCors(res) : res;
|
|
914
|
+
}
|
|
915
|
+
const baseUrl = resolveBaseUrl(req);
|
|
916
|
+
const res = await oauthIdentityProvider.callback(req, {
|
|
917
|
+
baseUrl: baseUrl + basePath,
|
|
918
|
+
});
|
|
919
|
+
return cors ? addCors(res) : res;
|
|
920
|
+
}
|
|
921
|
+
|
|
676
922
|
// ── GET /health → Health check ──
|
|
677
923
|
if (path === "/health" && req.method === "GET") {
|
|
678
924
|
const res = jsonResponse({ status: "ok", agents: registry.listPaths() });
|
|
@@ -690,14 +936,15 @@ export function createAgentServer(
|
|
|
690
936
|
|
|
691
937
|
// ── GET /.well-known/configuration → Server discovery ──
|
|
692
938
|
if (path === "/.well-known/configuration" && req.method === "GET") {
|
|
693
|
-
const baseUrl =
|
|
939
|
+
const baseUrl = resolveBaseUrl(req);
|
|
694
940
|
const res = jsonResponse({
|
|
695
941
|
issuer: baseUrl,
|
|
696
942
|
jwks_uri: `${baseUrl}/.well-known/jwks.json`,
|
|
697
943
|
token_endpoint: `${baseUrl}/oauth/token`,
|
|
698
944
|
agents_endpoint: `${baseUrl}/list`,
|
|
699
945
|
call_endpoint: baseUrl,
|
|
700
|
-
supported_grant_types: ["client_credentials"],
|
|
946
|
+
supported_grant_types: ["client_credentials", "jwt_exchange"],
|
|
947
|
+
authorization_endpoint: `${baseUrl}/oauth/authorize`,
|
|
701
948
|
agents: registry.listPaths(),
|
|
702
949
|
});
|
|
703
950
|
return cors ? addCors(res) : res;
|
|
@@ -769,11 +1016,11 @@ export function createAgentServer(
|
|
|
769
1016
|
url: null,
|
|
770
1017
|
registry,
|
|
771
1018
|
|
|
772
|
-
async
|
|
773
|
-
// Load or generate signing
|
|
774
|
-
if (options.signingKey) {
|
|
1019
|
+
async initKeys() {
|
|
1020
|
+
// Load or generate signing keys (without starting Bun.serve)
|
|
1021
|
+
if (options.signingKey && serverSigningKeys.length === 0) {
|
|
775
1022
|
serverSigningKeys.push(options.signingKey);
|
|
776
|
-
} else if (authConfig?.store?.getSigningKeys) {
|
|
1023
|
+
} else if (authConfig?.store?.getSigningKeys && serverSigningKeys.length === 0) {
|
|
777
1024
|
const stored = await authConfig.store.getSigningKeys() ?? [];
|
|
778
1025
|
for (const exported of stored) {
|
|
779
1026
|
serverSigningKeys.push(await importSigningKey(exported));
|
|
@@ -786,6 +1033,10 @@ export function createAgentServer(
|
|
|
786
1033
|
await authConfig.store.storeSigningKey(await exportSigningKey(key));
|
|
787
1034
|
}
|
|
788
1035
|
}
|
|
1036
|
+
},
|
|
1037
|
+
|
|
1038
|
+
async start() {
|
|
1039
|
+
await this.initKeys();
|
|
789
1040
|
|
|
790
1041
|
serverInstance = Bun.serve({
|
|
791
1042
|
port,
|
|
@@ -805,5 +1056,28 @@ export function createAgentServer(
|
|
|
805
1056
|
},
|
|
806
1057
|
|
|
807
1058
|
fetch,
|
|
1059
|
+
|
|
1060
|
+
async signJwt(claims: Record<string, unknown>): Promise<string> {
|
|
1061
|
+
if (serverSigningKeys.length === 0) {
|
|
1062
|
+
throw new Error('No signing keys available. Call start() or initKeys() first.');
|
|
1063
|
+
}
|
|
1064
|
+
const key = serverSigningKeys[0];
|
|
1065
|
+
return signJwtES256(
|
|
1066
|
+
{ sub: 'system', name: 'atlas-os', scopes: ['*'], ...claims } as any,
|
|
1067
|
+
key.privateKey,
|
|
1068
|
+
key.kid,
|
|
1069
|
+
options.serverName ?? 'agents-sdk',
|
|
1070
|
+
'1h',
|
|
1071
|
+
);
|
|
1072
|
+
},
|
|
1073
|
+
|
|
1074
|
+
addTrustedIssuer(issuerUrl: string, scopes?: string[]): void {
|
|
1075
|
+
// Avoid duplicates
|
|
1076
|
+
const existing = configTrustedIssuers.find(i => i.issuer === issuerUrl);
|
|
1077
|
+
if (!existing) {
|
|
1078
|
+
configTrustedIssuers.push({ issuer: issuerUrl, scopes: scopes ?? ['*'] });
|
|
1079
|
+
console.error(`[agent-server] Added trusted issuer: ${issuerUrl}`);
|
|
1080
|
+
}
|
|
1081
|
+
},
|
|
808
1082
|
};
|
|
809
1083
|
}
|
package/src/types.ts
CHANGED
|
@@ -20,6 +20,11 @@ export type JsonSchema = {
|
|
|
20
20
|
enum?: unknown[];
|
|
21
21
|
default?: unknown;
|
|
22
22
|
additionalProperties?: boolean | JsonSchema;
|
|
23
|
+
/** Agent refs (paths to other agents this agent can call) */
|
|
24
|
+
refs?: Record<string, { description?: string }>;
|
|
25
|
+
|
|
26
|
+
/** Tools to expose publicly (by name) */
|
|
27
|
+
public?: { tools?: string[] };
|
|
23
28
|
[key: string]: unknown;
|
|
24
29
|
};
|
|
25
30
|
|
|
@@ -73,6 +78,30 @@ export interface IntegrationMethods {
|
|
|
73
78
|
update(params: Record<string, unknown>, ctx: IntegrationMethodContext): Promise<IntegrationMethodResult>;
|
|
74
79
|
}
|
|
75
80
|
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
/** Hooks for agents that implement the integrations interface. */
|
|
84
|
+
export interface IntegrationHooks {
|
|
85
|
+
/** Provider metadata */
|
|
86
|
+
provider: string;
|
|
87
|
+
displayName: string;
|
|
88
|
+
icon?: string;
|
|
89
|
+
category?: string;
|
|
90
|
+
description?: string;
|
|
91
|
+
|
|
92
|
+
/** Set up this integration (discover, configure, establish trust) */
|
|
93
|
+
setup?(params: Record<string, unknown>, ctx: ToolContext): Promise<IntegrationMethodResult>;
|
|
94
|
+
/** Connect a user to this integration (OAuth, identity linking) */
|
|
95
|
+
connect?(params: Record<string, unknown>, ctx: ToolContext): Promise<IntegrationMethodResult>;
|
|
96
|
+
/** Discover available instances of this integration */
|
|
97
|
+
discover?(params: Record<string, unknown>, ctx: ToolContext): Promise<IntegrationMethodResult>;
|
|
98
|
+
/** List connected instances */
|
|
99
|
+
list?(params: Record<string, unknown>, ctx: ToolContext): Promise<IntegrationMethodResult>;
|
|
100
|
+
/** Get details of a specific instance */
|
|
101
|
+
get?(params: Record<string, unknown>, ctx: ToolContext): Promise<IntegrationMethodResult>;
|
|
102
|
+
/** Update an existing instance config */
|
|
103
|
+
update?(params: Record<string, unknown>, ctx: ToolContext): Promise<IntegrationMethodResult>;
|
|
104
|
+
}
|
|
76
105
|
export interface IntegrationConfig {
|
|
77
106
|
/** Provider identifier (e.g., "databases", "slack", "github") */
|
|
78
107
|
provider: string;
|
|
@@ -134,6 +163,11 @@ export interface AgentConfig {
|
|
|
134
163
|
modelParams?: {
|
|
135
164
|
maxTokens?: number;
|
|
136
165
|
temperature?: number;
|
|
166
|
+
/** Agent refs (paths to other agents this agent can call) */
|
|
167
|
+
refs?: Record<string, { description?: string }>;
|
|
168
|
+
|
|
169
|
+
/** Tools to expose publicly (by name) */
|
|
170
|
+
public?: { tools?: string[] };
|
|
137
171
|
[key: string]: unknown;
|
|
138
172
|
};
|
|
139
173
|
|
|
@@ -145,6 +179,11 @@ export interface AgentConfig {
|
|
|
145
179
|
integration?: IntegrationConfig;
|
|
146
180
|
|
|
147
181
|
/** Additional configuration */
|
|
182
|
+
/** Agent refs (paths to other agents this agent can call) */
|
|
183
|
+
refs?: Record<string, { description?: string }>;
|
|
184
|
+
|
|
185
|
+
/** Tools to expose publicly (by name) */
|
|
186
|
+
public?: { tools?: string[] };
|
|
148
187
|
[key: string]: unknown;
|
|
149
188
|
}
|
|
150
189
|
|
|
@@ -197,6 +236,11 @@ export interface ToolContext extends CoreContext {
|
|
|
197
236
|
callerType: CallerType;
|
|
198
237
|
|
|
199
238
|
/** Application-specific extensions (e.g., OS services, stores) */
|
|
239
|
+
/** Agent refs (paths to other agents this agent can call) */
|
|
240
|
+
refs?: Record<string, { description?: string }>;
|
|
241
|
+
|
|
242
|
+
/** Tools to expose publicly (by name) */
|
|
243
|
+
public?: { tools?: string[] };
|
|
200
244
|
[key: string]: unknown;
|
|
201
245
|
}
|
|
202
246
|
|
|
@@ -492,7 +536,6 @@ export interface AgentDefinition<TContext extends ToolContext = ToolContext> {
|
|
|
492
536
|
* When set alongside config.integration, this agent can be called
|
|
493
537
|
* by @integrations via standard methods (setup, list, connect, get, update).
|
|
494
538
|
*/
|
|
495
|
-
integrationMethods?: IntegrationMethods;
|
|
496
539
|
|
|
497
540
|
/**
|
|
498
541
|
* Lazy loader for lifecycle listeners.
|