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
4 changes: 4 additions & 0 deletions apps/presentation/dashboard/src/data/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2302,6 +2302,7 @@ export async function fetchLarkGroupChats(appRef: string, query?: string) {
export type LarkGoalConnection = {
conversation_kind?: "goal" | "manager";
turn_trigger?: "addressed" | "human_messages";
read_goal_scope?: "all_registered" | "selected" | null;
agent_id: string | null;
connection_id: string;
app_label: string;
Expand Down Expand Up @@ -2334,6 +2335,7 @@ const larkConnectionsSchema = z.object({
connections: z.array(z.object({
conversation_kind: z.enum(["goal", "manager"]).default("goal"),
turn_trigger: z.enum(["addressed", "human_messages"]).default("addressed"),
read_goal_scope: z.enum(["all_registered", "selected"]).nullable().default("selected"),
agent_id: z.string().nullable().default(null),
connection_id: z.string(),
app_label: z.string(),
Expand Down Expand Up @@ -2381,6 +2383,7 @@ export async function fetchLarkConnections() {
export async function connectLarkGoalTopic(options: {
conversationKind?: "goal" | "manager";
turnTrigger?: "addressed" | "human_messages";
readGoalScope?: "all_registered" | "selected";
agentBindings?: Array<{ agentId: string; appRef: string }>;
agentId?: string;
appRef?: string;
Expand Down Expand Up @@ -2409,6 +2412,7 @@ export async function connectLarkGoalTopic(options: {
...(options.connectionId ? { connection_id: options.connectionId } : {}),
conversation_kind: options.conversationKind ?? "goal",
...(options.turnTrigger ? { turn_trigger: options.turnTrigger } : {}),
...(options.readGoalScope ? { read_goal_scope: options.readGoalScope } : {}),
capture_scope: options.captureScope,
chat_id: options.chatId,
chat_name: options.chatName,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -820,6 +820,11 @@ const en = {
"lark.triggerAddressed": "Only when mentioned or replied to",
"lark.triggerHuman": "Respond to group members without @",
"lark.triggerDescription": "Everyone in this connected group can start a conversation. Bot messages and historical backfill stay as context. Requires the Lark app permission to receive all group messages; tool and delegation permissions stay unchanged.",
"lark.readGoalScope": "Goal evidence access",
"lark.readAllGoals": "All registered Goals, including new ones",
"lark.readSelectedGoals": "Keep the selected Goal scope",
"lark.readScopeUnavailable": "Goal read scope unavailable",
"lark.readGoalScopeDescription": "Applies to everyone authorized to use this manager conversation. This reads local Goal evidence; it grants no execution, publishing or remote-host access. Existing connections retain their selected scope until you change it.",
"lark.managerConversationDescription": "The built-in manager responds as you chat and delegates long-running work to background Agents. Group and private conversations keep separate histories.",
"lark.ingressLegacy": "Upgrade available",
"lark.ingressLegacyDescription": "Save this connection to upgrade to the Agent inbox. Existing messages remain in the same Topic.",
Expand Down Expand Up @@ -2392,6 +2397,11 @@ const zhCN: Record<WorkspaceMessageKey, string> = {
"lark.triggerAddressed": "仅 @ 或回复管家时",
"lark.triggerHuman": "群成员直接发消息,无需 @",
"lark.triggerDescription": "此群所有成员都可以发起对话;机器人消息和补拉历史只作背景。需要飞书应用具备接收群内所有消息的权限;工具权限和委派授权保持不变。",
"lark.readGoalScope": "Goal 证据读取范围",
"lark.readAllGoals": "全部已注册 Goal(含后续新增)",
"lark.readSelectedGoals": "保留选定 Goal 范围",
"lark.readScopeUnavailable": "Goal 读取范围不可用",
"lark.readGoalScopeDescription": "适用于获准使用此管家对话的所有成员。只读取本机 Goal 证据,不授予执行、发布或远程主机权限。旧连接保留选定范围,直到你明确修改。",
"lark.managerConversationDescription": "内置管家即时回应对话,长任务交给后台 Agent。群聊与私聊分别保存上下文。",
"lark.ingressLegacy": "待升级",
"lark.ingressLegacyDescription": "保存连接即可升级为 Agent 异步收件箱,已有消息保留在原 Topic 中。",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export function LarkSettingsPage({
const [chatLoading, setChatLoading] = useState(false);
const [chatLoadError, setChatLoadError] = useState<string | null>(null);
const [turnTrigger, setTurnTrigger] = useState<"addressed" | "human_messages">("addressed");
const [readGoalScope, setReadGoalScope] = useState<"all_registered" | "selected">("all_registered");
const [captureScope, setCaptureScope] = useState<LarkCaptureScope>("addressed_only");
const [conversationKind, setConversationKind] = useState<"goal" | "manager">(focusGoalConnection && initialGoalId ? "goal" : "manager");
const [ingressMode, setIngressMode] = useState<LarkIngressMode>("async_inbox");
Expand Down Expand Up @@ -404,6 +405,7 @@ export function LarkSettingsPage({
setAgentAppRefs({});
setCaptureScope("addressed_only");
setTurnTrigger("addressed");
setReadGoalScope("all_registered");
setIngressMode("async_inbox");
setReplyMode("topic_reply");
setChatQuery("");
Expand All @@ -424,6 +426,7 @@ export function LarkSettingsPage({
setAgentAppRefs({});
setCaptureScope(connection.capture_scope);
setTurnTrigger(connection.turn_trigger ?? "addressed");
setReadGoalScope(connection.read_goal_scope ?? "selected");
setIngressMode(connection.ingress_mode === "direct_session" ? "async_inbox" : connection.ingress_mode);
setReplyMode(connection.reply_mode);
setChatQuery(connection.chat_name);
Expand Down Expand Up @@ -492,7 +495,7 @@ export function LarkSettingsPage({
chatName: selectedChat!.chat_name,
}),
conversationKind,
...(conversationKind === "manager" ? { turnTrigger } : {}),
...(conversationKind === "manager" ? { turnTrigger, readGoalScope } : {}),
captureScope: conversationKind === "manager" ? "addressed_only" as const : captureScope,
goalId,
incomingMode: captureScope === "configured_chat_all" ? "all" as const : "mentions" as const,
Expand Down Expand Up @@ -590,7 +593,7 @@ export function LarkSettingsPage({
<a href={larkGroupHistoryPermissionUrl(connection) ?? undefined} rel="noreferrer" target="_blank"><ExternalLink size={12} />{t("lark.historyPermission")}</a>
) : null}
</span>
<span><strong>{connection.goal_title}</strong><small># {connection.topic_name}</small></span>
<span><strong>{connection.goal_title}</strong><small># {connection.topic_name}</small>{connection.conversation_kind === "manager" ? <small>{t(connection.read_goal_scope === "all_registered" ? "lark.readAllGoals" : connection.read_goal_scope === null ? "lark.readScopeUnavailable" : "lark.readSelectedGoals")}</small> : null}</span>
<span>{connection.conversation_kind === "manager" ? t(connection.turn_trigger === "human_messages" ? "lark.triggerHuman" : "lark.triggerAddressed") : connection.capture_scope === "addressed_only" ? t("lark.mentionsOnly") : t("lark.allTopicMessages")}</span>
<span><strong>{connection.conversation_kind === "manager" ? t("lark.managerConversation") : ingressPresentation(connection.ingress_mode, t).label}</strong><small>{connection.conversation_kind === "manager" ? t("lark.managerConversationDescription") : connection.agent_id ?? ingressPresentation(connection.ingress_mode, t).detail}</small></span>
<span className="personal-lark-row-actions">
Expand Down Expand Up @@ -631,6 +634,7 @@ export function LarkSettingsPage({
<label className="personal-lark-check"><input checked readOnly type="checkbox" /><span><strong>{t("lark.createAutomatically")}</strong><small>{t("lark.createAutomaticallyDescription")}</small></span></label>
<label><span>{t("lark.topicPreview")}</span><div className="personal-lark-topic-preview"><MessageSquareText size={15} /># {selectedGoal?.title ?? selectedGoal?.goalId ?? "Goal"}</div></label>
{conversationKind === "manager" ? <label><span>{t("lark.turnTrigger")}</span><select aria-label={t("lark.turnTrigger")} value={turnTrigger} onChange={(event) => setTurnTrigger(event.target.value as "addressed" | "human_messages")}><option value="addressed">{t("lark.triggerAddressed")}</option><option value="human_messages">{t("lark.triggerHuman")}</option></select><small>{t("lark.triggerDescription")}</small></label> : null}
{conversationKind === "manager" ? <label><span>{t("lark.readGoalScope")}</span><select aria-label={t("lark.readGoalScope")} value={readGoalScope} onChange={(event) => setReadGoalScope(event.target.value as "all_registered" | "selected")}><option value="all_registered">{t("lark.readAllGoals")}</option><option value="selected">{t("lark.readSelectedGoals")}</option></select><small>{t("lark.readGoalScopeDescription")}</small></label> : null}
{conversationKind === "goal" ? <>
<label><span>{t("lark.captureScope")}</span><select aria-label={t("lark.captureScope")} disabled={editingConnection?.ingress_mode === "direct_session"} onChange={(event) => setCaptureScope(event.target.value as LarkCaptureScope)} value={captureScope}><option value="addressed_only">{t("lark.captureAddressed")}</option><option value="configured_chat_all">{t("lark.captureAll")}</option></select><small>{t("lark.captureScopeDescription")}</small></label>
<fieldset aria-label={t("lark.agentIngress")} className="personal-lark-ingress"><legend>{t("lark.agentIngress")}</legend><div>{(["live_steering", "session_queue", "async_inbox"] as const).map((mode) => { const presentation = ingressPresentation(mode, t); return <label className={ingressMode === mode ? "is-active" : ""} key={mode}><input aria-label={presentation.label} checked={ingressMode === mode} name="lark-agent-ingress" onChange={() => setIngressMode(mode)} type="radio" value={mode} /><span><strong>{presentation.label}</strong><small>{presentation.detail}</small></span></label>; })}</div></fieldset>
Expand Down
3 changes: 2 additions & 1 deletion examples/personal-workspace-browser/fixture.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1503,7 +1503,7 @@ export async function installApi(page, { fixturePort = port, goalSubagentConfigu
const existing = runtime.larkConnections.find((item) => item.connection_id === body.connection_id && item.goal_id === body.goal_id);
if (!existing || body.app_ref || body.chat_id || body.agent_bindings) throw new Error("Editing must select the stored connection without replacing its identity");
if (body.execute) {
Object.assign(existing, { turn_trigger: body.turn_trigger ?? existing.turn_trigger, agent_id: body.agent_id, ingress_mode: body.ingress_mode, capture_scope: body.capture_scope });
Object.assign(existing, { read_goal_scope: body.read_goal_scope ?? existing.read_goal_scope, turn_trigger: body.turn_trigger ?? existing.turn_trigger, agent_id: body.agent_id, ingress_mode: body.ingress_mode, capture_scope: body.capture_scope });
state.larkWrites.push({ ...body });
}
await route.fulfill({ contentType: "application/json", json: { ok: true, status: body.execute ? "connected" : "preview_ready" }, status: 200 });
Expand All @@ -1522,6 +1522,7 @@ export async function installApi(page, { fixturePort = port, goalSubagentConfigu
agent_id: body.conversation_kind === "manager" ? "loopx-manager" : binding.agent_id ?? null,
conversation_kind: body.conversation_kind ?? "goal",
turn_trigger: body.turn_trigger ?? "addressed",
read_goal_scope: body.read_goal_scope ?? "all_registered",
connection_id: connectionId,
app_label: binding.app_ref === "mew-research" ? "LoopX Research" : "LoopX Mew", app_ref: binding.app_ref, chat_name: body.chat_name, enabled: true,
capture_scope: body.capture_scope,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export const stewardGroupTriggerScenario = {
const dialog = page.getByRole("dialog", {name: "连接 Lark App"});
await dialog.getByRole("option", {name: "Product group"}).waitFor({state: "attached"});
await dialog.getByLabel("群聊").selectOption({label: "Product group"});
const readScope = dialog.getByLabel("Goal 证据读取范围");
if (await readScope.inputValue() !== "all_registered") throw new Error("New steward does not default to all registered evidence");
const trigger = dialog.getByLabel("何时回应");
if (await trigger.inputValue() !== "addressed") throw new Error("Legacy mention default changed");
await trigger.selectOption("human_messages");
Expand All @@ -23,6 +25,7 @@ export const stewardGroupTriggerScenario = {
const row = page.locator(".personal-lark-table-row", {hasText: "Product group"});
await row.getByText("群成员直接发消息,无需 @", {exact: true}).waitFor();
await row.getByText("监听已连接,尚无新消息验证这条连接。请在群里直接发一条任务,无需 @;若没有收到事件,再检查 im.message.receive_v1 和接收群内所有消息的权限。", {exact: true}).waitFor();
if (api.larkWrites[0]?.read_goal_scope !== "all_registered") throw new Error("Evidence scope not saved");
if (api.larkWrites.length !== 1 || api.larkWrites[0].turn_trigger !== "human_messages") throw new Error("Trigger not saved");
const connection = api.larkConnections[0];
for (const [reason, detail] of [
Expand All @@ -46,10 +49,14 @@ export const stewardGroupTriggerScenario = {
await page.setViewportSize({width: 390, height: 844});
await editor.getByLabel("何时回应").scrollIntoViewIfNeeded();
await page.screenshot({path: resolve(outputDir, "steward-group-trigger-mobile.png"), animations: "disabled"});
if (await editor.getByLabel("Goal 证据读取范围").inputValue() !== "all_registered") throw new Error("Evidence scope not read back");
await editor.getByLabel("Goal 证据读取范围").selectOption("selected");
await editor.getByLabel("何时回应").selectOption("addressed");
await editor.getByRole("button", {name: "保存连接", exact: true}).click();
await editor.waitFor({state: "hidden"});
await row.getByText("仅 @ 或回复管家时", {exact: true}).waitFor();
await row.getByText("保留选定 Goal 范围", {exact: true}).waitFor();
if (api.larkWrites[1]?.read_goal_scope !== "selected") throw new Error("Evidence scope could not be narrowed");
if (api.larkWrites[1]?.turn_trigger !== "addressed") throw new Error("Trigger not restored");
if (context.errors.length) throw new Error(context.errors.join(" | "));
return {coverageEntries: await context.close(), note: "Steward group trigger opt-in/readback/revocation and typed non-execution feedback; desktop and narrow screen; scripted provider state only"};
Expand Down
25 changes: 25 additions & 0 deletions loopx/capabilities/manager_context/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,31 @@ path; this does not add a visible settings control or launch workers. Prompt-onl
adapters still have no interactive discovery tool. Live multi-worker adoption and
original-conversation completion require separate qualification.

Local owner managers read all registered Goals by default. A bounded Turn index
is a summary, not an authorization list: on-demand Todo, delivery, Agent and
portfolio reads use the current audience scope. Portfolio pagination selects the
permitted registry page before collecting evidence, including Goals registered
after the initial Turn. Goal conversations remain confined to their selected
Goal; external conversations are rechecked before and after each read.

New manager connections created in Settings → Lark default to **All registered
Goals, including new ones**. This applies to the connection's authorized audience,
including its group members. Existing connections retain their selected read
scope. Edit the existing connection's **Goal evidence access** to opt into the
live registered inventory or return to its selected scope. The UI reads back the
actual policy and preserves failures; changing this read setting does not grant
worker execution, publishing, context delivery or SSH access.

The same policy is available through the existing trusted-local CLI. Omitted
`--read-goal-scope` preserves the selected-list behavior of existing scripts;
an empty selected list revokes the read grant. Old lists are not automatically
widened during installation.

```sh
loopx manager-inbox configure-read-scope --channel-id manager.external.0123456789abcdef01234567 --read-goal-scope all_registered --execute
loopx manager-inbox configure-read-scope --channel-id manager.external.0123456789abcdef01234567 --read-goal-scope selected --read-goal-id project-a --execute
```

Routine inspection excludes Goals explicitly stopped in Core, before status
collection and detail reads. Coverage reports how many were skipped. Stale or
unknown progress remains eligible. An explicit historical question can discover
Expand Down
Loading
Loading