Skip to content
Merged
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
1 change: 1 addition & 0 deletions changelog.d/545-secrets-all-my-bots.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A key you save, including the GitHub token and the Composio key a connector saves, is now for all of your Bots unless you narrow it; keys that were "No Bot" only because of the old connector default move to all of your Bots once. Settings → Secrets offers All my Bots, Chosen Bots or No Bot for each key ([#545](https://github.com/ThinkFlowLab/FlyBot/issues/545)).
22 changes: 12 additions & 10 deletions core/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -761,7 +761,7 @@ var ZH = {
Dismissed: "已关闭",
"Your answer": "你的回答",
// Secrets
"All Bots": "所有 Bot",
"All my Bots": "我的所有 Bot",
"No Bot": "没有 Bot",
"Paste or type the key": "粘贴或输入密钥",
"Only {name}": "只给 {name}",
Expand Down Expand Up @@ -804,7 +804,7 @@ var ZH = {
"NAME, e.g. GITHUB_TOKEN": "名字,例如 GITHUB_TOKEN",
"What it is for": "用途",
Value: "值",
"New keys are for all Bots; narrow it down afterwards.": "新密钥默认所有 Bot 可用,之后可以收窄。",
"New keys are for all your Bots; change who may use it afterwards.": "新密钥默认你的所有 Bot 可用,之后可以更改谁能用。",
"Save key": "保存密钥",
"Keys your Bots asked for. Bots never see the values: they use them as ": "你的 Bot 请求过的密钥。Bot 永远看不到值:它们在 shell 里通过 ",
" in their shell, and every value is replaced by ": " 使用,而且发给模型之前,每个值都会被替换成 ",
Expand Down Expand Up @@ -4697,7 +4697,7 @@ var WarnIcon = () => (0, import_react6.createElement)(
var MASK_BY_CSS = typeof CSS !== "undefined" && typeof CSS.supports === "function" && CSS.supports("-webkit-text-security", "disc");
var insecure = () => typeof location !== "undefined" && location.protocol === "http:" && !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname);
var scopeLabel = (scope, roster) => {
if (scope === "all") return t("All Bots");
if (scope === "all") return t("All my Bots");
const names = (scope ?? []).map((id) => roster.byId[id]?.name).filter(Boolean);
return names.length === 0 ? t("No Bot") : names.join(t(" and "));
};
Expand Down Expand Up @@ -4728,7 +4728,7 @@ function MaskedInput({ value, onChange, onEnter, label, autoFocus = false }) {
});
}
function ScopeSwitch({ value, onChange, bot }) {
const options = [["all", t("All Bots")], ["bot", t("Only {name}", { name: bot.name })]];
const options = [["all", t("All my Bots")], ["bot", t("Only {name}", { name: bot.name })]];
return (0, import_react6.createElement)(
"div",
{ className: "bt-secret-scope", role: "radiogroup", "aria-label": t("Who may use it") },
Expand Down Expand Up @@ -4860,19 +4860,21 @@ function SecretDock({ sessionId, useRoster, useCards, useSession, actions }) {
if (waiting.length === 0) return null;
return (0, import_react6.createElement)("div", { className: "bt-q-dock bt-secret-dock" }, waiting.map((request) => (0, import_react6.createElement)(SecretCard, { key: `${request.id}:${request.kind}`, bot, request, roster, actions })));
}
var scopeMode = (scope) => scope === "all" ? "all" : scope.length === 0 ? "none" : "some";
function ScopeEditor({ secret, roster, onSave }) {
const [mode, setMode] = (0, import_react6.useState)(secret.scope === "all" ? "all" : "some");
const [mode, setMode] = (0, import_react6.useState)(scopeMode(secret.scope));
const [picked, setPicked] = (0, import_react6.useState)(secret.scope === "all" ? [] : secret.scope);
const toggle = (id) => setPicked((list) => list.includes(id) ? list.filter((item) => item !== id) : [...list, id]);
const same2 = mode === "all" ? secret.scope === "all" : secret.scope !== "all" && [...picked].sort().join() === [...secret.scope].sort().join();
const next = mode === "all" ? "all" : mode === "none" ? [] : picked;
const same2 = next === "all" ? secret.scope === "all" : secret.scope !== "all" && [...next].sort().join() === [...secret.scope].sort().join();
return (0, import_react6.createElement)(
"div",
{ className: "bt-secret-edit-block" },
(0, import_react6.createElement)("div", { className: "bt-secret-edit-label" }, t("Who may use it")),
(0, import_react6.createElement)(
"div",
{ className: "bt-secret-radios", role: "radiogroup", "aria-label": t("Who may use {name}", { name: secret.name }) },
[["all", t("All Bots")], ["some", t("Chosen Bots")]].map(([id, text]) => (0, import_react6.createElement)(
[["all", t("All my Bots")], ["some", t("Chosen Bots")], ["none", t("No Bot")]].map(([id, text]) => (0, import_react6.createElement)(
"label",
{ key: id, className: "bt-secret-radio" },
(0, import_react6.createElement)("input", { type: "radio", name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }),
Expand All @@ -4889,7 +4891,7 @@ function ScopeEditor({ secret, roster, onSave }) {
(0, import_react6.createElement)(
"div",
{ className: "bt-secret-row-actions" },
(0, import_react6.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same2, onClick: () => onSave(mode === "all" ? "all" : picked) }, t("Save access"))
(0, import_react6.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: same2, onClick: () => onSave(next) }, t("Save access"))
)
);
}
Expand Down Expand Up @@ -4946,7 +4948,7 @@ function SecretRow({ secret, roster, actions, run, open, onToggle }) {
(0, import_react6.createElement)(
"span",
{ className: "bt-set-control" },
secret.scope === "all" ? (0, import_react6.createElement)("span", { className: "bt-tag" }, t("All Bots")) : allowed.length === 0 ? (0, import_react6.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react6.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react6.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false })))
secret.scope === "all" ? (0, import_react6.createElement)("span", { className: "bt-tag" }, t("All my Bots")) : allowed.length === 0 ? (0, import_react6.createElement)("span", { className: "bt-tag" }, t("No Bot")) : (0, import_react6.createElement)("span", { className: "bt-secret-faces", title: allowed.map((bot) => bot.name).join(", ") }, allowed.slice(0, 5).map((bot) => (0, import_react6.createElement)(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false })))
)
),
open ? (0, import_react6.createElement)(
Expand Down Expand Up @@ -4989,7 +4991,7 @@ function AddSecret({ actions, run, onDone }) {
(0, import_react6.createElement)(
"div",
{ className: "bt-secret-row-actions" },
(0, import_react6.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all Bots; narrow it down afterwards.")),
(0, import_react6.createElement)("span", { className: "bt-set-hint" }, t("New keys are for all your Bots; change who may use it afterwards.")),
(0, import_react6.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", onClick: onDone }, t("Cancel")),
(0, import_react6.createElement)("button", { type: "button", className: "bt-soft bt-soft-sm", disabled: !ready, onClick: save }, t("Save key"))
)
Expand Down
4 changes: 2 additions & 2 deletions core/src/client/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -707,7 +707,7 @@ const ZH = {
'Your answer': '你的回答',

// Secrets
'All Bots': '所有 Bot',
'All my Bots': '我的所有 Bot',
'No Bot': '没有 Bot',
'Paste or type the key': '粘贴或输入密钥',
'Only {name}': '只给 {name}',
Expand Down Expand Up @@ -750,7 +750,7 @@ const ZH = {
'NAME, e.g. GITHUB_TOKEN': '名字,例如 GITHUB_TOKEN',
'What it is for': '用途',
Value: '值',
'New keys are for all Bots; narrow it down afterwards.': '新密钥默认所有 Bot 可用,之后可以收窄。',
'New keys are for all your Bots; change who may use it afterwards.': '新密钥默认你的所有 Bot 可用,之后可以更改谁能用。',
'Save key': '保存密钥',
'Keys your Bots asked for. Bots never see the values: they use them as ': '你的 Bot 请求过的密钥。Bot 永远看不到值:它们在 shell 里通过 ',
' in their shell, and every value is replaced by ': ' 使用,而且发给模型之前,每个值都会被替换成 ',
Expand Down
22 changes: 13 additions & 9 deletions core/src/client/secret-card.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const insecure = () => typeof location !== 'undefined' && location.protocol ===
&& !/^(localhost|127(?:\.\d{1,3}){3}|\[::1\])$/.test(location.hostname)

const scopeLabel = (scope, roster) => {
if (scope === 'all') return t('All Bots')
if (scope === 'all') return t('All my Bots')
const names = (scope ?? []).map(id => roster.byId[id]?.name).filter(Boolean)
return names.length === 0 ? t('No Bot') : names.join(t(' and '))
}
Expand Down Expand Up @@ -60,7 +60,7 @@ export function MaskedInput({ value, onChange, onEnter, label, autoFocus = false
}

function ScopeSwitch({ value, onChange, bot }) {
const options = [['all', t('All Bots')], ['bot', t('Only {name}', { name: bot.name })]]
const options = [['all', t('All my Bots')], ['bot', t('Only {name}', { name: bot.name })]]
return h('div', { className: 'bt-secret-scope', role: 'radiogroup', 'aria-label': t('Who may use it') },
h('span', { className: 'bt-secret-glide', 'data-at': value === 'all' ? 0 : 1, 'aria-hidden': true }),
options.map(([id, text]) => h('button', {
Expand Down Expand Up @@ -170,21 +170,25 @@ export function SecretDock({ sessionId, useRoster, useCards, useSession, actions
// -------------------------------------------------------------------------
// Settings page

function ScopeEditor({ secret, roster, onSave }) {
const [mode, setMode] = useState(secret.scope === 'all' ? 'all' : 'some')
// All my Bots (the default for every key the owner saves, #545), chosen Bots, or none.
const scopeMode = scope => (scope === 'all' ? 'all' : scope.length === 0 ? 'none' : 'some')

export function ScopeEditor({ secret, roster, onSave }) {
const [mode, setMode] = useState(scopeMode(secret.scope))
const [picked, setPicked] = useState(secret.scope === 'all' ? [] : secret.scope)
const toggle = id => setPicked(list => (list.includes(id) ? list.filter(item => item !== id) : [...list, id]))
const same = mode === 'all' ? secret.scope === 'all' : secret.scope !== 'all' && [...picked].sort().join() === [...secret.scope].sort().join()
const next = mode === 'all' ? 'all' : mode === 'none' ? [] : picked
const same = next === 'all' ? secret.scope === 'all' : secret.scope !== 'all' && [...next].sort().join() === [...secret.scope].sort().join()
return h('div', { className: 'bt-secret-edit-block' },
h('div', { className: 'bt-secret-edit-label' }, t('Who may use it')),
h('div', { className: 'bt-secret-radios', role: 'radiogroup', 'aria-label': t('Who may use {name}', { name: secret.name }) },
[['all', t('All Bots')], ['some', t('Chosen Bots')]].map(([id, text]) => h('label', { key: id, className: 'bt-secret-radio' },
[['all', t('All my Bots')], ['some', t('Chosen Bots')], ['none', t('No Bot')]].map(([id, text]) => h('label', { key: id, className: 'bt-secret-radio' },
h('input', { type: 'radio', name: `scope-${secret.name}`, checked: mode === id, onChange: () => setMode(id) }), text))),
mode === 'some' ? h('div', { className: 'bt-secret-bots' }, roster.bots.map(bot => h('button', {
key: bot.id, type: 'button', className: 'bt-secret-bot', 'aria-pressed': picked.includes(bot.id), onClick: () => toggle(bot.id),
}, h(BotAvatar, { bot, size: 20, badge: false, live: false }), bot.name, picked.includes(bot.id) ? h(CheckIcon) : null))) : null,
h('div', { className: 'bt-secret-row-actions' },
h('button', { type: 'button', className: 'bt-soft bt-soft-sm', disabled: same, onClick: () => onSave(mode === 'all' ? 'all' : picked) }, t('Save access'))))
h('button', { type: 'button', className: 'bt-soft bt-soft-sm', disabled: same, onClick: () => onSave(next) }, t('Save access'))))
}

function ValueEditor({ secret, onSave }) {
Expand Down Expand Up @@ -217,7 +221,7 @@ function SecretRow({ secret, roster, actions, run, open, onToggle }) {
h('span', { className: 'bt-secret-title' }, h(KeyIcon), h('code', { className: 'bt-secret-name' }, secret.name), secret.purpose ? h('span', { className: 'bt-secret-purpose' }, secret.purpose) : null),
h('span', { className: 'bt-set-hint' }, meta)),
h('span', { className: 'bt-set-control' },
secret.scope === 'all' ? h('span', { className: 'bt-tag' }, t('All Bots'))
secret.scope === 'all' ? h('span', { className: 'bt-tag' }, t('All my Bots'))
: allowed.length === 0 ? h('span', { className: 'bt-tag' }, t('No Bot'))
: h('span', { className: 'bt-secret-faces', title: allowed.map(bot => bot.name).join(', ') }, allowed.slice(0, 5).map(bot => h(BotAvatar, { key: bot.id, bot, size: 20, badge: false, live: false }))))),
open ? h('div', { className: 'bt-secret-edit' },
Expand Down Expand Up @@ -247,7 +251,7 @@ function AddSecret({ actions, run, onDone }) {
h('input', { className: 'bt-input', 'aria-label': t('What it is for'), placeholder: t('What it is for'), value: purpose, autoComplete: 'off', onChange: event => setPurpose(event.target.value) }),
h('label', { className: 'bt-q-field bt-secret-field' }, h(MaskedInput, { value, onChange: setValue, onEnter: save, label: t('Value') })),
h('div', { className: 'bt-secret-row-actions' },
h('span', { className: 'bt-set-hint' }, t('New keys are for all Bots; narrow it down afterwards.')),
h('span', { className: 'bt-set-hint' }, t('New keys are for all your Bots; change who may use it afterwards.')),
h('button', { type: 'button', className: 'bt-soft bt-soft-sm', onClick: onDone }, t('Cancel')),
h('button', { type: 'button', className: 'bt-soft bt-soft-sm', disabled: !ready, onClick: save }, t('Save key'))))
}
Expand Down
12 changes: 6 additions & 6 deletions core/src/host/connectors.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
//
// A secret (GITHUB_TOKEN, COMPOSIO_API_KEY) is kept and redacted by secrets.js. It reaches
// the service only in the MCP request headers, built here in memory, never written to the
// DSH profile. A new secret is shared with no Bot's shell: the tools do not need it there,
// and a Bot that reads a planted instruction in an issue or a mail could send it anywhere.
// The Secrets page can still share it. Signing in is `check` alone, so a browser login can
// take its place later without touching the mount.
// DSH profile. A new secret is for all of the owner's Bots, like any key the owner saves
// (owner decision 2026-10-20, #359, #545): their shells get it as $DSH_SECRET_<NAME> too.
// The Secrets page narrows it to chosen Bots or to none. Reconnecting keeps the scope the
// key already has. Signing in is `check` alone, so a browser login can take its place later
// without touching the mount.

import { randomUUID } from 'node:crypto'

Expand Down Expand Up @@ -426,12 +427,11 @@ export function install(rt) {
if (c.insecure) throw new Error(INSECURE(c.def.name))
await rt.loadSecrets()
const oldToken = rt.secretValue(c.def.secret)
const kept = oldToken !== undefined
const previous = saved(c)
const record = await c.def.check({ token, options: c.options, saved: previous, body })
c.saving += 1
try {
await rt.secretEndpoint('secret-set', { name: c.def.secret, value: token, purpose: c.def.purpose, ...(kept ? {} : { scope: [] }) })
await rt.secretEndpoint('secret-set', { name: c.def.secret, value: token, purpose: c.def.purpose })
state.connectors = { ...state.connectors, [c.def.id]: { ...record, connectedAt: Date.now() } }
await rt.save()
} catch (error) {
Expand Down
Loading
Loading