Skip to content

Commit 879bf93

Browse files
committed
fix: let secret handoff choose scope
1 parent 87c951d commit 879bf93

3 files changed

Lines changed: 79 additions & 32 deletions

File tree

packages/plugins/openapi/src/sdk/plugin.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,9 +284,10 @@ describe("OpenAPI Plugin", () => {
284284
{ spec: testApiSpec() },
285285
autoApprove,
286286
),
287-
).data as { operationCount: number };
287+
).data as { operationCount: number; operations?: unknown };
288288

289289
expect(preview.operationCount).toBeGreaterThanOrEqual(2);
290+
expect(preview.operations).toBeUndefined();
290291
}),
291292
);
292293

@@ -305,6 +306,7 @@ describe("OpenAPI Plugin", () => {
305306
expect(schema!.outputTypeScript).toContain("securitySchemes:");
306307
expect(schema!.outputTypeScript).toContain("oauth2Presets:");
307308
expect(schema!.outputTypeScript).toContain("title: string | null");
309+
expect(schema!.outputTypeScript).not.toContain("operations:");
308310
expect(schema!.outputTypeScript).not.toContain("_tag");
309311
}),
310312
);

packages/plugins/openapi/src/sdk/plugin.ts

Lines changed: 1 addition & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -249,14 +249,6 @@ const PreviewSpecInputSchema = Schema.Struct({
249249
),
250250
});
251251

252-
const StaticPreviewOperationSchema = Schema.Struct({
253-
operationId: Schema.String,
254-
method: Schema.Literals(["get", "put", "post", "delete", "patch", "head", "options", "trace"]),
255-
path: Schema.String,
256-
summary: Schema.NullOr(Schema.String),
257-
tags: Schema.Array(Schema.String),
258-
deprecated: Schema.Boolean,
259-
});
260252
const StaticPreviewServerVariableSchema = Schema.Struct({
261253
default: Schema.String,
262254
enum: Schema.NullOr(Schema.Array(Schema.String)),
@@ -307,7 +299,6 @@ const StaticPreviewSpecOutputSchema = Schema.Struct({
307299
version: Schema.NullOr(Schema.String),
308300
servers: Schema.Array(StaticPreviewServerSchema),
309301
operationCount: Schema.Number,
310-
operations: Schema.Array(StaticPreviewOperationSchema),
311302
tags: Schema.Array(Schema.String),
312303
securitySchemes: Schema.Array(StaticPreviewSecuritySchemeSchema),
313304
authStrategies: Schema.Array(Schema.Struct({ schemes: Schema.Array(Schema.String) })),
@@ -469,14 +460,6 @@ const staticPreviewOutput = (preview: SpecPreview): StaticPreviewSpecOutput => (
469460
: null,
470461
})),
471462
operationCount: preview.operationCount,
472-
operations: preview.operations.map((operation) => ({
473-
operationId: operation.operationId,
474-
method: operation.method,
475-
path: operation.path,
476-
summary: Option.getOrNull(operation.summary),
477-
tags: operation.tags,
478-
deprecated: operation.deprecated,
479-
})),
480463
tags: preview.tags,
481464
securitySchemes: preview.securitySchemes.map((scheme) => ({
482465
name: scheme.name,
@@ -1431,7 +1414,7 @@ export const openApiPlugin = definePlugin((options?: OpenApiPluginOptions) => {
14311414
tool({
14321415
name: "previewSpec",
14331416
description:
1434-
"Preview an OpenAPI document before adding it as a source. Call this first when the user provides a spec URL/blob so you can inspect servers, auth schemes, operation count, and credential slots before `addSource`. Do not collect API keys or OAuth client secrets in chat; use `executor.coreTools.secrets.create` for those values.",
1417+
"Preview an OpenAPI document before adding it as a source. Call this first when the user provides a spec URL/blob so you can inspect servers, auth schemes, operation count, tags, and credential slots before `addSource`. This agent-facing preview intentionally omits the full operations list; use `operationCount` and `tags` for full-size specs. Do not collect API keys or OAuth client secrets in chat; use `executor.coreTools.secrets.create` for those values.",
14351418
inputSchema: PreviewSpecInputStandardSchema,
14361419
outputSchema: PreviewSpecOutputStandardSchema,
14371420
execute: (input) =>

packages/react/src/pages/secrets.tsx

Lines changed: 75 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,13 @@ import {
2222
DialogClose,
2323
} from "../components/dialog";
2424
import { Button } from "../components/button";
25+
import {
26+
Select,
27+
SelectContent,
28+
SelectItem,
29+
SelectTrigger,
30+
SelectValue,
31+
} from "../components/select";
2532
import {
2633
DropdownMenu,
2734
DropdownMenuContent,
@@ -39,12 +46,18 @@ import {
3946
CardStackHeader,
4047
} from "../components/card-stack";
4148
import { Badge } from "../components/badge";
49+
import { cn } from "../lib/utils";
4250

4351
type SecretStorageOption = {
4452
readonly label: string;
4553
readonly value: string;
4654
};
4755

56+
type SecretScopeOption = {
57+
readonly label: string;
58+
readonly value: ScopeId;
59+
};
60+
4861
const defaultStorageOptions: readonly SecretStorageOption[] = [
4962
{ value: "auto", label: "Auto" },
5063
{ value: "keychain", label: "Keychain" },
@@ -77,8 +90,9 @@ function AddSecretDialog(props: {
7790
onOpenChange: (v: boolean) => void;
7891
description: string;
7992
storageOptions: readonly SecretStorageOption[];
80-
existingSecretIds: readonly string[];
93+
existingSecrets: readonly { readonly id: string; readonly scopeId: ScopeId }[];
8194
scopeId: ScopeId;
95+
scopeOptions: readonly SecretScopeOption[];
8296
prefill?: SecretPrefill;
8397
}) {
8498
return (
@@ -88,8 +102,9 @@ function AddSecretDialog(props: {
88102
key="open"
89103
description={props.description}
90104
storageOptions={props.storageOptions}
91-
existingSecretIds={props.existingSecretIds}
105+
existingSecrets={props.existingSecrets}
92106
scopeId={props.scopeId}
107+
scopeOptions={props.scopeOptions}
93108
prefill={props.prefill}
94109
onClose={() => props.onOpenChange(false)}
95110
/>
@@ -101,20 +116,31 @@ function AddSecretDialog(props: {
101116
function AddSecretDialogContent(props: {
102117
description: string;
103118
storageOptions: readonly SecretStorageOption[];
104-
existingSecretIds: readonly string[];
119+
existingSecrets: readonly { readonly id: string; readonly scopeId: ScopeId }[];
105120
scopeId: ScopeId;
121+
scopeOptions: readonly SecretScopeOption[];
106122
prefill?: SecretPrefill;
107123
onClose: () => void;
108124
}) {
109125
const initialProvider = props.prefill?.provider ?? props.storageOptions[0]?.value ?? "auto";
126+
const [targetScope, setTargetScope] = useState(props.scopeId);
127+
const existingSecretIds = useMemo(
128+
() =>
129+
props.existingSecrets
130+
.filter((secret) => secret.scopeId === targetScope)
131+
.map((secret) => secret.id),
132+
[props.existingSecrets, targetScope],
133+
);
134+
const controlFieldClassName =
135+
"[&_[data-slot=field-label]]:h-5 [&_[data-slot=field-label]]:items-start [&_[data-slot=field-label]]:leading-none [&_[data-slot=input]]:h-9";
110136

111137
return (
112138
<SecretForm.Provider
113-
existingSecretIds={props.existingSecretIds}
139+
existingSecretIds={existingSecretIds}
114140
suggestedName={props.prefill?.name}
115141
initialIdOverride={props.prefill?.secretId}
116142
initialProvider={initialProvider}
117-
scopeId={props.scopeId}
143+
scopeId={targetScope}
118144
onCreated={props.onClose}
119145
>
120146
<DialogContent className="sm:max-w-[440px]">
@@ -126,11 +152,37 @@ function AddSecretDialogContent(props: {
126152
</DialogHeader>
127153

128154
<div className="grid gap-5 py-3">
129-
<div className="grid grid-cols-2 gap-3">
130-
<SecretForm.NameField />
131-
<SecretForm.IdField />
155+
<div className="grid grid-cols-2 items-start gap-3">
156+
<div className={controlFieldClassName}>
157+
<SecretForm.NameField />
158+
</div>
159+
<div className={controlFieldClassName}>
160+
<SecretForm.IdField />
161+
</div>
162+
</div>
163+
<div className="grid grid-cols-2 items-start gap-3">
164+
<div className={controlFieldClassName}>
165+
<SecretForm.ValueField />
166+
</div>
167+
<div className={cn("grid min-w-0 gap-3", controlFieldClassName)}>
168+
<label className="h-5 text-sm font-medium leading-none">Scope</label>
169+
<Select
170+
value={targetScope}
171+
onValueChange={(value) => setTargetScope(ScopeId.make(value))}
172+
>
173+
<SelectTrigger className="h-9 w-full min-w-0 text-sm">
174+
<SelectValue />
175+
</SelectTrigger>
176+
<SelectContent>
177+
{props.scopeOptions.map((option) => (
178+
<SelectItem key={option.value} value={option.value}>
179+
{option.label}
180+
</SelectItem>
181+
))}
182+
</SelectContent>
183+
</Select>
184+
</div>
132185
</div>
133-
<SecretForm.ValueField />
134186
<SecretForm.ProviderField options={props.storageOptions} />
135187
<SecretForm.ErrorBanner />
136188
</div>
@@ -273,12 +325,21 @@ export function SecretsPage(props: {
273325
const existingSecretIds = useMemo(
274326
() =>
275327
AsyncResult.match(secrets, {
276-
onInitial: () => [] as string[],
277-
onFailure: () => [] as string[],
278-
onSuccess: ({ value }) => value.map((secret) => secret.id),
328+
onInitial: () => [] as { readonly id: string; readonly scopeId: ScopeId }[],
329+
onFailure: () => [] as { readonly id: string; readonly scopeId: ScopeId }[],
330+
onSuccess: ({ value }) =>
331+
value.map((secret) => ({ id: secret.id, scopeId: secret.scopeId })),
279332
}),
280333
[secrets],
281334
);
335+
const scopeOptions = useMemo(
336+
() =>
337+
scopeStack.map((entry, index) => ({
338+
value: entry.id,
339+
label: index === 0 ? "Personal" : entry.name || "Organization",
340+
})),
341+
[scopeStack],
342+
);
282343
const doRemove = useAtomSet(removeSecretOptimistic(scopeId), {
283344
mode: "promiseExit",
284345
});
@@ -414,8 +475,9 @@ export function SecretsPage(props: {
414475
onOpenChange={setAddOpen}
415476
description={addSecretDescription}
416477
storageOptions={storageOptions}
417-
existingSecretIds={existingSecretIds}
478+
existingSecrets={existingSecretIds}
418479
scopeId={formScopeId}
480+
scopeOptions={scopeOptions}
419481
prefill={props.prefill}
420482
/>
421483
</div>

0 commit comments

Comments
 (0)