Skip to content
Open
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: 2 additions & 0 deletions src/bundled/goose/goose-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,8 @@ export function fakeSpawn(
options.saved?.set(providerId, set);
return reply(id, { status: { providerId, isConfigured: true } });
}
case "_goose/unstable/providers/inventory/refresh":
return reply(id, { started: params.providerIds, skipped: [] });
case "_goose/unstable/providers/supported-models/list":
return reply(id, {
providerId: params.providerId,
Expand Down
6 changes: 5 additions & 1 deletion src/bundled/goose/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,11 @@ export const apply: PluginModule["apply"] = (ctx) => {
resolveDirectory: host.resolveDirectory.bind(host),
}
: undefined;
const setup = new GooseSetup(desktop, () => runtime.defaultsChanged());
const setup = new GooseSetup(
desktop,
() => runtime.defaultsChanged(),
(provider) => runtime.credentialsChanged(provider),
);
const history = new GooseHistory(desktop);
const {
Create,
Expand Down
15 changes: 15 additions & 0 deletions src/bundled/goose/runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,21 @@ describe("GooseRuntime", () => {
},
);

it("has the running goose reload a provider's saved key", async () => {
const { deliver, fake, runtime } = setup();
await runtime.credentialsChanged("openrouter");
expect(fake.geese).toHaveLength(0);
await deliver(message("1", "@Gander hi"));
await flush(100);
await runtime.credentialsChanged("openrouter");
expect(fake.geese).toHaveLength(1);
const [goose] = fake.geese;
const refresh = goose?.received.find((item) =>
item.method?.endsWith("inventory/refresh"),
);
expect(refresh?.params).toEqual({ providerIds: ["openrouter"] });
});

it("runs a mention on the agent's thinking level", async () => {
const { deliver, fake } = setup();
await deliver(message("1", "@Gander think"), {
Expand Down
5 changes: 5 additions & 0 deletions src/bundled/goose/runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ export class GooseRuntime {
if (!entry.config.provider.trim()) entry.sessions.reconfigure();
}

/** Has the shared goose reload a provider's key saved by Settings. */
credentialsChanged(provider: string) {
return this.goose.refresh(provider);
}

sync(agents: readonly Agent[]) {
const current = new Set(agents.map((agent) => agent.pubkey));
for (const [pubkey, entry] of this.agents)
Expand Down
15 changes: 15 additions & 0 deletions src/bundled/goose/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
// sessions stay listed after they close.
import {
GooseProcess,
REQUEST_TIMEOUT_MS,
type ActivityReceipt,
type Choice,
type GooseHost,
Expand Down Expand Up @@ -117,6 +118,20 @@ export class SharedProcess {
this.ready = ready;
return ready;
}
/** goose caches secrets per process, so a key saved by another one reaches
* this one only once an inventory refresh reloads them. Its sessions then
* rebuild their provider on their next call. Best effort: a goose that
* fails to answer is killed, and the next one starts with fresh secrets. */
async refresh(provider: string) {
const goose = await this.ready?.catch(() => undefined);
await goose
?.request(
"_goose/unstable/providers/inventory/refresh",
{ providerIds: [provider] },
REQUEST_TIMEOUT_MS,
)
.catch(() => undefined);
}
/** Stops it, failing every session in it. */
stop() {
const ready = this.ready;
Expand Down
14 changes: 14 additions & 0 deletions src/bundled/goose/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,20 @@ it("invalidates runtime defaults only after a successful save", async () => {
expect(changed).toHaveBeenCalledTimes(1);
});

it("reports a provider's saved key before the save resolves", async () => {
const saved = new Map<string, Set<string>>();
const changed = vi.fn(async (provider: string) => {
expect(saved.get(provider)).toEqual(new Set(["ANTHROPIC_API_KEY"]));
});
const goose = new GooseSetup(
fakeSpawn({ providers: PROVIDERS, saved }),
undefined,
changed,
);
await goose.save("anthropic", { ANTHROPIC_API_KEY: "sk-test" });
expect(changed).toHaveBeenCalledExactlyOnceWith("anthropic");
});

it("blocks dismissal throughout credential persistence and clears blocking on failure", async () => {
const { setup: goose } = setup();
await goose.check();
Expand Down
5 changes: 5 additions & 0 deletions src/bundled/goose/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@ export class GooseSetup {
constructor(
private readonly host: GooseHost | undefined,
private readonly defaultsChanged: () => void = () => {},
/** Called once a provider's fields are saved, before `save` resolves. */
private readonly credentialsChanged: (
provider: string,
) => Promise<void> = async () => {},
) {}

snapshot = () => this.state;
Expand Down Expand Up @@ -361,6 +365,7 @@ export class GooseSetup {
this.listing = undefined;
this.live.delete(id);
this.thinking.clear();
await this.credentialsChanged(id);
}

/** Asks the provider for one short reply, without tools. goose reports a
Expand Down