@slashfi/agents-sdk 0.87.0 → 0.89.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/cjs/config-store.js +50 -4
- package/dist/cjs/config-store.js.map +1 -1
- package/dist/config-store.d.ts.map +1 -1
- package/dist/config-store.js +50 -4
- package/dist/config-store.js.map +1 -1
- package/package.json +1 -1
- package/src/config-store.test.ts +333 -0
- package/src/config-store.ts +53 -5
package/src/config-store.test.ts
CHANGED
|
@@ -533,6 +533,93 @@ describe("ADK ref.call() full auto-refresh flow", () => {
|
|
|
533
533
|
expect((result as any)?.result?.token).toBe("refreshed-token");
|
|
534
534
|
});
|
|
535
535
|
|
|
536
|
+
test("ref.authStatus persists authFields={} in registry-cache for security:none refs (regression: isRefConnected miss-classifies auto-installed no-auth refs)", async () => {
|
|
537
|
+
// Regression: `authStatus` short-circuited at `security.type === "none"`
|
|
538
|
+
// (and at `security == null`) WITHOUT writing the slim `{required,
|
|
539
|
+
// automated}` authFields shape into `registry-cache.json`. Host-side
|
|
540
|
+
// `isRefAuthComplete` then returned `null` for those refs ("no
|
|
541
|
+
// authFields in cache"), and the LLM-facing `isRefConnected` filter
|
|
542
|
+
// in atlas-os-sdk fell back to a coarse `[access_token|api_key|token]`
|
|
543
|
+
// credential presence check — which a security:none ref like
|
|
544
|
+
// web-search/Firecrawl never has by definition. Result: auto-installed
|
|
545
|
+
// no-auth refs silently disappeared from `list_agents` and from
|
|
546
|
+
// `~/.adk/refs/` materialization.
|
|
547
|
+
//
|
|
548
|
+
// The fix: when `inspect` confirmed `security` is absent or
|
|
549
|
+
// `{type:"none"}`, persist `authFields: {}` so `isRefAuthComplete`
|
|
550
|
+
// returns `true` (no required fields to satisfy) and downstream
|
|
551
|
+
// filters treat the ref as connected.
|
|
552
|
+
//
|
|
553
|
+
// The `@math` agent registered above has no `config.security`, so it
|
|
554
|
+
// exercises the "registry returned no security field at all" path.
|
|
555
|
+
const fs = createMemoryFs();
|
|
556
|
+
const adk = createAdk(fs, {
|
|
557
|
+
encryptionKey: "test-key-32-chars-long-enough!!",
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
await adk.registry.add({
|
|
561
|
+
name: "oauth-reg",
|
|
562
|
+
url: `http://localhost:${REG_PORT}`,
|
|
563
|
+
});
|
|
564
|
+
await adk.ref.add({
|
|
565
|
+
ref: "@math",
|
|
566
|
+
name: "math",
|
|
567
|
+
sourceRegistry: {
|
|
568
|
+
url: `http://localhost:${REG_PORT}`,
|
|
569
|
+
agentPath: "@math",
|
|
570
|
+
},
|
|
571
|
+
});
|
|
572
|
+
|
|
573
|
+
const status = await adk.ref.authStatus("math");
|
|
574
|
+
expect(status.complete).toBe(true);
|
|
575
|
+
expect(status.fields).toEqual({});
|
|
576
|
+
|
|
577
|
+
// The cache must now carry authFields={} so isRefAuthComplete can
|
|
578
|
+
// answer "yes, ready to call" without re-fetching the security scheme.
|
|
579
|
+
const cacheRaw = await fs.readFile("registry-cache.json");
|
|
580
|
+
expect(cacheRaw).not.toBeNull();
|
|
581
|
+
const cache = JSON.parse(cacheRaw!);
|
|
582
|
+
expect(cache.refs.math).toBeDefined();
|
|
583
|
+
expect(cache.refs.math.authFields).toEqual({});
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
test("ref.authStatus does NOT persist authFields when inspect fails (registry unreachable)", async () => {
|
|
587
|
+
// Sibling guard: if the registry inspect call throws / returns null
|
|
588
|
+
// (network error, registry doesn't host the ref, etc.), we must NOT
|
|
589
|
+
// cache a false-positive `authFields: {}` — that would let the host
|
|
590
|
+
// treat an unreachable ref as "connected" on the next call.
|
|
591
|
+
const fs = createMemoryFs();
|
|
592
|
+
const adk = createAdk(fs, {
|
|
593
|
+
encryptionKey: "test-key-32-chars-long-enough!!",
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// Point at a port that nothing is listening on.
|
|
597
|
+
await adk.registry.add({
|
|
598
|
+
name: "dead-reg",
|
|
599
|
+
url: `http://localhost:1`,
|
|
600
|
+
});
|
|
601
|
+
await adk.ref.add({
|
|
602
|
+
ref: "@phantom",
|
|
603
|
+
name: "phantom",
|
|
604
|
+
sourceRegistry: {
|
|
605
|
+
url: `http://localhost:1`,
|
|
606
|
+
agentPath: "@phantom",
|
|
607
|
+
},
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
const status = await adk.ref.authStatus("phantom");
|
|
611
|
+
expect(status.complete).toBe(true);
|
|
612
|
+
expect(status.security).toBeNull();
|
|
613
|
+
|
|
614
|
+
// Registry was unreachable, so we shouldn't have written a cache
|
|
615
|
+
// entry that claims this ref is no-auth.
|
|
616
|
+
const cacheRaw = await fs.readFile("registry-cache.json");
|
|
617
|
+
if (cacheRaw !== null) {
|
|
618
|
+
const cache = JSON.parse(cacheRaw);
|
|
619
|
+
expect(cache.refs?.phantom?.authFields).toBeUndefined();
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
536
623
|
test("ref.authStatus reports access_token.automated=false for authorizationCode (user must consent)", async () => {
|
|
537
624
|
// Regression: previously `access_token.automated` was hardcoded to
|
|
538
625
|
// `true` for every oauth2 scheme. That made cached-authFields
|
|
@@ -566,6 +653,225 @@ describe("ADK ref.call() full auto-refresh flow", () => {
|
|
|
566
653
|
});
|
|
567
654
|
});
|
|
568
655
|
|
|
656
|
+
describe("ADK ref.call() auto-refresh on direct MCP 401", () => {
|
|
657
|
+
// Regression: refs whose entry has a direct `url` and `mode !== "api"`
|
|
658
|
+
// (Linear, Notion, Figma, DoorDash, Houzz, etc.) take the
|
|
659
|
+
// `callMcpDirect` branch instead of the registry-mediated `callRegistry`
|
|
660
|
+
// branch. Before this fix, a 401 from the upstream MCP server was
|
|
661
|
+
// surfaced as `{ success: false, error: "MCP tools/call failed (401):
|
|
662
|
+
// ..." }` with no `httpStatus` field, so `isUnauthorized(result)` never
|
|
663
|
+
// matched and the refresh-on-401 retry path in `ref.call` was silently
|
|
664
|
+
// skipped — even when the ref had a valid `refresh_token` on hand. The
|
|
665
|
+
// fix attaches `httpStatus` to the error envelope, restoring parity
|
|
666
|
+
// with the registry-mediated path (which already gets `_httpStatus`
|
|
667
|
+
// forwarded as structured data).
|
|
668
|
+
let registryServer: AgentServer;
|
|
669
|
+
let mcpServer: ReturnType<typeof Bun.serve>;
|
|
670
|
+
let tokenServer: ReturnType<typeof Bun.serve>;
|
|
671
|
+
const REG_PORT = 19930;
|
|
672
|
+
const MCP_PORT = 19931;
|
|
673
|
+
const TOKEN_PORT = 19932;
|
|
674
|
+
let toolCallCount = 0;
|
|
675
|
+
let tokenRefreshCount = 0;
|
|
676
|
+
let serverActiveToken = "";
|
|
677
|
+
|
|
678
|
+
beforeAll(async () => {
|
|
679
|
+
// Registry exposes the agent with an oauth2 security scheme so
|
|
680
|
+
// `ref.authStatus` can discover the tokenUrl that `refreshToken`
|
|
681
|
+
// POSTs to. The agent has no tools that ever get invoked here —
|
|
682
|
+
// the actual tool call goes direct to mcpServer below.
|
|
683
|
+
const stubTool = defineTool({
|
|
684
|
+
name: "some_tool",
|
|
685
|
+
description: "Never invoked via the registry in this test",
|
|
686
|
+
inputSchema: { type: "object" as const, properties: {} },
|
|
687
|
+
execute: async () => ({ message: "unused" }),
|
|
688
|
+
});
|
|
689
|
+
const agent = defineAgent({
|
|
690
|
+
path: "direct-mcp-agent",
|
|
691
|
+
entrypoint: "Direct-MCP agent (security discovery only)",
|
|
692
|
+
tools: [stubTool],
|
|
693
|
+
visibility: "public",
|
|
694
|
+
config: {
|
|
695
|
+
description: "Direct-MCP test agent (security discovery only)",
|
|
696
|
+
security: {
|
|
697
|
+
type: "oauth2",
|
|
698
|
+
flows: {
|
|
699
|
+
authorizationCode: {
|
|
700
|
+
authorizationUrl: "http://localhost/authorize",
|
|
701
|
+
tokenUrl: `http://localhost:${TOKEN_PORT}`,
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
},
|
|
705
|
+
},
|
|
706
|
+
});
|
|
707
|
+
const registry = createAgentRegistry();
|
|
708
|
+
registry.register(agent);
|
|
709
|
+
registryServer = createAgentServer(registry, { port: REG_PORT });
|
|
710
|
+
await registryServer.start();
|
|
711
|
+
|
|
712
|
+
// Direct MCP server — returns 401 unless the bearer token matches
|
|
713
|
+
// `serverActiveToken`. Real MCP servers signal 401 with an HTTP 401
|
|
714
|
+
// (not via httpStatus on the JSON-RPC body); that's exactly what the
|
|
715
|
+
// fix has to recover.
|
|
716
|
+
mcpServer = Bun.serve({
|
|
717
|
+
port: MCP_PORT,
|
|
718
|
+
async fetch(req) {
|
|
719
|
+
const body = (await req.json()) as {
|
|
720
|
+
method?: string;
|
|
721
|
+
id?: number;
|
|
722
|
+
params?: { name?: string };
|
|
723
|
+
};
|
|
724
|
+
const respond = (status: number, payload: unknown) =>
|
|
725
|
+
new Response(JSON.stringify(payload), {
|
|
726
|
+
status,
|
|
727
|
+
headers: { "Content-Type": "application/json" },
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
if (body.method === "initialize") {
|
|
731
|
+
return respond(200, {
|
|
732
|
+
jsonrpc: "2.0",
|
|
733
|
+
id: body.id,
|
|
734
|
+
result: {
|
|
735
|
+
protocolVersion: "2024-11-05",
|
|
736
|
+
capabilities: {},
|
|
737
|
+
serverInfo: { name: "mock-mcp", version: "1.0.0" },
|
|
738
|
+
},
|
|
739
|
+
});
|
|
740
|
+
}
|
|
741
|
+
if (body.method === "notifications/initialized") {
|
|
742
|
+
return respond(200, { jsonrpc: "2.0", id: body.id, result: {} });
|
|
743
|
+
}
|
|
744
|
+
if (body.method === "tools/call") {
|
|
745
|
+
toolCallCount++;
|
|
746
|
+
const auth = req.headers.get("Authorization") ?? "";
|
|
747
|
+
const token = auth.replace(/^Bearer /, "");
|
|
748
|
+
if (token !== serverActiveToken) {
|
|
749
|
+
return respond(401, {
|
|
750
|
+
jsonrpc: "2.0",
|
|
751
|
+
id: body.id,
|
|
752
|
+
error: { code: -32001, message: "Unauthorized" },
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
return respond(200, {
|
|
756
|
+
jsonrpc: "2.0",
|
|
757
|
+
id: body.id,
|
|
758
|
+
result: {
|
|
759
|
+
content: [
|
|
760
|
+
{
|
|
761
|
+
type: "text",
|
|
762
|
+
text: JSON.stringify({ ok: true, token }),
|
|
763
|
+
},
|
|
764
|
+
],
|
|
765
|
+
},
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
return respond(404, {
|
|
769
|
+
jsonrpc: "2.0",
|
|
770
|
+
id: body.id,
|
|
771
|
+
error: { code: -32601, message: "Method not found" },
|
|
772
|
+
});
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
|
|
776
|
+
// OAuth token endpoint — mints a fresh access token for a known refresh
|
|
777
|
+
// token + client_id. Rejects everything else with 400.
|
|
778
|
+
tokenServer = Bun.serve({
|
|
779
|
+
port: TOKEN_PORT,
|
|
780
|
+
async fetch(req) {
|
|
781
|
+
tokenRefreshCount++;
|
|
782
|
+
const params = new URLSearchParams(await req.text());
|
|
783
|
+
if (
|
|
784
|
+
params.get("grant_type") !== "refresh_token" ||
|
|
785
|
+
params.get("refresh_token") !== "direct-refresh-token" ||
|
|
786
|
+
params.get("client_id") !== "direct-client-id"
|
|
787
|
+
) {
|
|
788
|
+
return new Response(JSON.stringify({ error: "invalid_request" }), {
|
|
789
|
+
status: 400,
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
return new Response(
|
|
793
|
+
JSON.stringify({
|
|
794
|
+
access_token: "refreshed-direct-token",
|
|
795
|
+
token_type: "Bearer",
|
|
796
|
+
expires_in: 3600,
|
|
797
|
+
}),
|
|
798
|
+
{ headers: { "Content-Type": "application/json" } },
|
|
799
|
+
);
|
|
800
|
+
},
|
|
801
|
+
});
|
|
802
|
+
});
|
|
803
|
+
|
|
804
|
+
afterAll(async () => {
|
|
805
|
+
await registryServer.stop();
|
|
806
|
+
mcpServer.stop();
|
|
807
|
+
tokenServer.stop();
|
|
808
|
+
});
|
|
809
|
+
|
|
810
|
+
test("401 from direct MCP triggers refresh + retry (parity with registry-mediated refs)", async () => {
|
|
811
|
+
toolCallCount = 0;
|
|
812
|
+
tokenRefreshCount = 0;
|
|
813
|
+
// Server will only accept the refreshed token — the stale one we seed
|
|
814
|
+
// below must round-trip through refresh before the call can succeed.
|
|
815
|
+
serverActiveToken = "refreshed-direct-token";
|
|
816
|
+
|
|
817
|
+
const fs = createMemoryFs();
|
|
818
|
+
const adk = createAdk(fs, {
|
|
819
|
+
encryptionKey: "test-key-32-chars-long-enough!!",
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// Point the ref at the registry for security discovery, but also set
|
|
823
|
+
// a direct `url` so `ref.call` takes the `callMcpDirect` branch (the
|
|
824
|
+
// exact code path that was broken).
|
|
825
|
+
await adk.registry.add({
|
|
826
|
+
name: "direct-mcp-registry",
|
|
827
|
+
url: `http://localhost:${REG_PORT}`,
|
|
828
|
+
});
|
|
829
|
+
await adk.ref.add({
|
|
830
|
+
ref: "direct-mcp-agent",
|
|
831
|
+
url: `http://localhost:${MCP_PORT}`,
|
|
832
|
+
sourceRegistry: {
|
|
833
|
+
url: `http://localhost:${REG_PORT}`,
|
|
834
|
+
agentPath: "direct-mcp-agent",
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// Seed credentials directly. access_token is intentionally stale.
|
|
839
|
+
const config = await adk.readConfig();
|
|
840
|
+
await adk.writeConfig({
|
|
841
|
+
...config,
|
|
842
|
+
refs: config.refs?.map((r: any) => {
|
|
843
|
+
if (r.ref === "direct-mcp-agent") {
|
|
844
|
+
return {
|
|
845
|
+
...r,
|
|
846
|
+
// Force the direct-MCP branch: any mode that's not "api".
|
|
847
|
+
mode: "redirect",
|
|
848
|
+
config: {
|
|
849
|
+
...r.config,
|
|
850
|
+
access_token: "stale-direct-token",
|
|
851
|
+
refresh_token: "direct-refresh-token",
|
|
852
|
+
client_id: "direct-client-id",
|
|
853
|
+
},
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
return r;
|
|
857
|
+
}),
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
const result = await adk.ref.call("direct-mcp-agent", "some_tool", {});
|
|
861
|
+
|
|
862
|
+
// Without the fix: tokenRefreshCount stays 0, toolCallCount === 1,
|
|
863
|
+
// result.success === false with `MCP tools/call failed (401)` in error.
|
|
864
|
+
// With the fix: 401 → refresh → retry succeeds.
|
|
865
|
+
expect(tokenRefreshCount).toBe(1);
|
|
866
|
+
expect(toolCallCount).toBe(2);
|
|
867
|
+
expect((result as any).success).toBe(true);
|
|
868
|
+
expect((result as any).result).toEqual({
|
|
869
|
+
ok: true,
|
|
870
|
+
token: "refreshed-direct-token",
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
|
|
569
875
|
// ─── Registry auth lifecycle ─────────────────────────────────────
|
|
570
876
|
|
|
571
877
|
describe("ADK registry auth lifecycle", () => {
|
|
@@ -1099,6 +1405,33 @@ describe("isRefAuthComplete + cached authFields", () => {
|
|
|
1099
1405
|
expect(result).toBeNull();
|
|
1100
1406
|
});
|
|
1101
1407
|
|
|
1408
|
+
test("empty authFields object → true (security:none refs cache an empty map)", async () => {
|
|
1409
|
+
// Companion to the authStatus regression: an explicit `authFields: {}`
|
|
1410
|
+
// in the registry-cache (written by `authStatus` for security:none refs)
|
|
1411
|
+
// means "no required fields to satisfy" — not "cache miss". The
|
|
1412
|
+
// distinction matters because callers (`atlas-os-sdk` `isRefConnected`)
|
|
1413
|
+
// use a different fallback strategy on null vs false. With an empty
|
|
1414
|
+
// map, the required-fields loop runs zero times and we return true.
|
|
1415
|
+
const { isRefAuthComplete } = await import("./config-store");
|
|
1416
|
+
const result = isRefAuthComplete(
|
|
1417
|
+
{
|
|
1418
|
+
ref: "@web-search",
|
|
1419
|
+
name: "@web-search",
|
|
1420
|
+
scheme: "registry",
|
|
1421
|
+
sourceRegistry: {
|
|
1422
|
+
url: "http://localhost",
|
|
1423
|
+
agentPath: "@web-search",
|
|
1424
|
+
},
|
|
1425
|
+
},
|
|
1426
|
+
{
|
|
1427
|
+
ref: "@web-search",
|
|
1428
|
+
fetchedAt: new Date().toISOString(),
|
|
1429
|
+
authFields: {},
|
|
1430
|
+
},
|
|
1431
|
+
);
|
|
1432
|
+
expect(result).toBe(true);
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1102
1435
|
test("required field present → true", async () => {
|
|
1103
1436
|
const { isRefAuthComplete } = await import("./config-store");
|
|
1104
1437
|
const result = isRefAuthComplete(
|
package/src/config-store.ts
CHANGED
|
@@ -959,6 +959,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
959
959
|
return data;
|
|
960
960
|
}
|
|
961
961
|
|
|
962
|
+
/**
|
|
963
|
+
* Error thrown by `callMcpDirect` when the upstream MCP server returns a
|
|
964
|
+
* non-2xx HTTP response. Carries the numeric `status` so the catch handler
|
|
965
|
+
* can surface it as `httpStatus` on the returned CallAgentResponse, which
|
|
966
|
+
* `isUnauthorized` (and the retry-on-401 path in `ref.call`) relies on.
|
|
967
|
+
*/
|
|
968
|
+
class McpHttpError extends Error {
|
|
969
|
+
readonly status: number;
|
|
970
|
+
constructor(status: number, message: string) {
|
|
971
|
+
super(message);
|
|
972
|
+
this.name = "McpHttpError";
|
|
973
|
+
this.status = status;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
|
|
962
977
|
/** Call an MCP server directly (bypasses registry). */
|
|
963
978
|
async function callMcpDirect(
|
|
964
979
|
serverUrl: string,
|
|
@@ -993,7 +1008,8 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
993
1008
|
}),
|
|
994
1009
|
});
|
|
995
1010
|
if (!res.ok) {
|
|
996
|
-
throw new
|
|
1011
|
+
throw new McpHttpError(
|
|
1012
|
+
res.status,
|
|
997
1013
|
`MCP ${method} failed (${res.status}): ${await res.text().catch(() => "unknown")}`,
|
|
998
1014
|
);
|
|
999
1015
|
}
|
|
@@ -1066,10 +1082,21 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
1066
1082
|
}
|
|
1067
1083
|
return { success: true, result } as CallAgentResponse;
|
|
1068
1084
|
} catch (err) {
|
|
1069
|
-
|
|
1070
|
-
|
|
1085
|
+
// Preserve upstream HTTP status (notably 401) so `isUnauthorized`
|
|
1086
|
+
// can detect it and trigger the auto-refresh-and-retry path in
|
|
1087
|
+
// `ref.call`. Without this, refs that go through callMcpDirect
|
|
1088
|
+
// (mode: redirect/proxy with an MCP url, e.g. Linear, Notion) see
|
|
1089
|
+
// their tokens expire and fail with a raw 401 instead of silently
|
|
1090
|
+
// refreshing the way API-mode refs (Google, etc.) do via the
|
|
1091
|
+
// registry's structured response. We attach httpStatus as an
|
|
1092
|
+
// out-of-band field on the error envelope, matching the shape
|
|
1093
|
+
// `isUnauthorized` already checks for on registry-mediated calls.
|
|
1094
|
+
const errorResponse = {
|
|
1095
|
+
success: false as const,
|
|
1071
1096
|
error: err instanceof Error ? err.message : String(err),
|
|
1072
|
-
|
|
1097
|
+
...(err instanceof McpHttpError && { httpStatus: err.status }),
|
|
1098
|
+
};
|
|
1099
|
+
return errorResponse as unknown as CallAgentResponse;
|
|
1073
1100
|
}
|
|
1074
1101
|
}
|
|
1075
1102
|
|
|
@@ -2229,6 +2256,7 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2229
2256
|
if (!entry) throw new Error(`Ref "${name}" not found`);
|
|
2230
2257
|
|
|
2231
2258
|
let security: SecuritySchemeSummary | null = null;
|
|
2259
|
+
let inspectSucceeded = false;
|
|
2232
2260
|
try {
|
|
2233
2261
|
const consumer = await buildConsumerForRef(entry);
|
|
2234
2262
|
// Pass `sourceRegistry.url` so inspect targets the registry the ref
|
|
@@ -2241,12 +2269,32 @@ export function createAdk(fs: FsStore, options: AdkOptions = {}): Adk {
|
|
|
2241
2269
|
entry.sourceRegistry?.agentPath ?? entry.ref,
|
|
2242
2270
|
entry.sourceRegistry?.url,
|
|
2243
2271
|
);
|
|
2244
|
-
if (info
|
|
2272
|
+
if (info) {
|
|
2273
|
+
inspectSucceeded = true;
|
|
2274
|
+
if (info.security) security = info.security;
|
|
2275
|
+
}
|
|
2245
2276
|
} catch {
|
|
2246
2277
|
// Can't reach registry
|
|
2247
2278
|
}
|
|
2248
2279
|
|
|
2249
2280
|
if (!security || security.type === "none") {
|
|
2281
|
+
// Persist an empty authFields map when the registry confirmed the
|
|
2282
|
+
// ref needs no auth — either an explicit `security.type === "none"`
|
|
2283
|
+
// or no `security` field on the agent at all. Host-side filters
|
|
2284
|
+
// that consult the registry-cache (e.g. atlas-os-sdk
|
|
2285
|
+
// `isRefConnected`) need this to distinguish "registry says this
|
|
2286
|
+
// ref needs no auth" from "we never warmed the cache". Without
|
|
2287
|
+
// it, auto-installed no-auth refs (e.g. web-search/Firecrawl)
|
|
2288
|
+
// look identical to never-inspected refs and get filtered out
|
|
2289
|
+
// of LLM-facing surfaces as "not connected" even though they
|
|
2290
|
+
// have nothing to connect.
|
|
2291
|
+
//
|
|
2292
|
+
// Gate on `inspectSucceeded` so we don't cache a false positive
|
|
2293
|
+
// when the registry was unreachable (network failure / consumer
|
|
2294
|
+
// error — `inspect` returned null/threw).
|
|
2295
|
+
if (inspectSucceeded) {
|
|
2296
|
+
await upsertRegistryCacheAuthFields(name, entry.ref, {});
|
|
2297
|
+
}
|
|
2250
2298
|
return { name, security, complete: true, fields: {} };
|
|
2251
2299
|
}
|
|
2252
2300
|
|