@saadnvd1/agent-os 0.3.496 → 0.3.497
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/README.md +9 -0
- package/app/api/mcp-servers/route.ts +8 -0
- package/app/api/workspaces/[id]/route.ts +11 -2
- package/components/Workspaces/McpServersForm.tsx +54 -0
- package/components/Workspaces/WorkspaceSettings.tsx +2 -0
- package/data/workspaces/index.ts +23 -1
- package/lib/chat/runner.ts +14 -0
- package/lib/db/migrations.ts +14 -0
- package/lib/db/types.ts +2 -0
- package/lib/orchestrator/brief.ts +2 -0
- package/lib/orchestrator/mcp-servers.ts +93 -0
- package/lib/orchestrator/tools.ts +4 -2
- package/lib/orchestrator/worker.ts +8 -3
- package/lib/workspaces.ts +28 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1113,6 +1113,15 @@ it may do on its own, and the lines that always come back to you as asks.
|
|
|
1113
1113
|
written, so an orchestrator misled by text it read could run code that way.
|
|
1114
1114
|
That's accepted: task agents can already write anywhere, so an allowlist
|
|
1115
1115
|
on the orchestrator alone would buy little.
|
|
1116
|
+
- **MCP servers.** An orchestrator runs without prompts, so an MCP server's
|
|
1117
|
+
tools are refused unless its workspace allows the server. Settings →
|
|
1118
|
+
Workspaces lists the servers configured on the machine (names only: user
|
|
1119
|
+
servers, connectors and plugin servers; never their env, headers or tokens)
|
|
1120
|
+
as toggles, and `linear` is allowed by default. A ticked server adds a bare
|
|
1121
|
+
`mcp__<server>` rule to the orchestrator's allowed tools, covering all its
|
|
1122
|
+
tools. Its worker ends at its next turn boundary when the list changes, and
|
|
1123
|
+
the next one starts with the new permissions. Sending a message, email or
|
|
1124
|
+
post through a server stays a hard line the brief sends to `ask_saad`.
|
|
1116
1125
|
Its chat worker checks every write in a PreToolUse hook before it runs, on
|
|
1117
1126
|
the path with symlinks and `..` resolved as the system will, and refuses
|
|
1118
1127
|
with the reason; the repositories are read from the database on each write,
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { configuredMcpServers } from "@/lib/orchestrator/mcp-servers";
|
|
3
|
+
|
|
4
|
+
// The names of the MCP servers configured on this machine, for choosing which
|
|
5
|
+
// a workspace's orchestrator may use. Names only.
|
|
6
|
+
export async function GET() {
|
|
7
|
+
return NextResponse.json({ servers: configuredMcpServers() });
|
|
8
|
+
}
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { deleteWorkspace, updateWorkspace } from "@/lib/workspaces";
|
|
3
|
+
import { applyMcpServers } from "@/lib/orchestrator/mcp-servers";
|
|
3
4
|
|
|
4
5
|
type RouteParams = { params: Promise<{ id: string }> };
|
|
5
6
|
|
|
6
7
|
export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
|
7
8
|
const { id } = await params;
|
|
8
|
-
const {
|
|
9
|
-
|
|
9
|
+
const {
|
|
10
|
+
name,
|
|
11
|
+
collapsed,
|
|
12
|
+
sortOrder,
|
|
13
|
+
maxRunningTasks,
|
|
14
|
+
wakeTasksOnPR,
|
|
15
|
+
mcpServers,
|
|
16
|
+
} = await request.json();
|
|
10
17
|
if (wakeTasksOnPR !== undefined && typeof wakeTasksOnPR !== "boolean")
|
|
11
18
|
return NextResponse.json(
|
|
12
19
|
{ error: "wakeTasksOnPR is true or false" },
|
|
@@ -20,6 +27,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
|
|
20
27
|
sortOrder,
|
|
21
28
|
maxRunningTasks,
|
|
22
29
|
wakeTasksOnPR,
|
|
30
|
+
mcpServers,
|
|
23
31
|
});
|
|
24
32
|
} catch (error) {
|
|
25
33
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -28,6 +36,7 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
|
|
28
36
|
if (!workspace) {
|
|
29
37
|
return NextResponse.json({ error: "Workspace not found" }, { status: 404 });
|
|
30
38
|
}
|
|
39
|
+
if (mcpServers !== undefined) await applyMcpServers(id);
|
|
31
40
|
return NextResponse.json({ workspace });
|
|
32
41
|
}
|
|
33
42
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { Switch } from "@/components/ui/switch";
|
|
4
|
+
import { useMcpServersQuery, useUpdateWorkspace } from "@/data/workspaces";
|
|
5
|
+
import type { Workspace } from "@/lib/db";
|
|
6
|
+
|
|
7
|
+
// Which MCP servers a workspace's orchestrator may use. Servers carry their
|
|
8
|
+
// own auth, so none is allowed unless ticked here.
|
|
9
|
+
export function McpServersForm({ workspace }: { workspace: Workspace }) {
|
|
10
|
+
const { data: configured = [] } = useMcpServersQuery();
|
|
11
|
+
const update = useUpdateWorkspace();
|
|
12
|
+
const allowed = workspace.orch_mcp_servers;
|
|
13
|
+
// A ticked server no longer configured stays listed, so it can be unticked.
|
|
14
|
+
const names = [...new Set([...configured, ...allowed])];
|
|
15
|
+
|
|
16
|
+
const toggle = (name: string, on: boolean) =>
|
|
17
|
+
update.mutate({
|
|
18
|
+
id: workspace.id,
|
|
19
|
+
mcpServers: on ? [...allowed, name] : allowed.filter((n) => n !== name),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="bg-muted/40 space-y-2 rounded-lg px-3 py-2.5">
|
|
24
|
+
<div>
|
|
25
|
+
<p className="text-sm font-medium">
|
|
26
|
+
MCP servers its orchestrator may use
|
|
27
|
+
</p>
|
|
28
|
+
<p className="text-muted-foreground text-xs">
|
|
29
|
+
All of a ticked server's tools are allowed. Sending a message,
|
|
30
|
+
email or post through one still goes to you as an ask. Takes effect at
|
|
31
|
+
its next turn.
|
|
32
|
+
</p>
|
|
33
|
+
</div>
|
|
34
|
+
{names.length === 0 ? (
|
|
35
|
+
<p className="text-muted-foreground text-xs">
|
|
36
|
+
No MCP servers are configured on this machine.
|
|
37
|
+
</p>
|
|
38
|
+
) : (
|
|
39
|
+
names.map((name) => (
|
|
40
|
+
<label
|
|
41
|
+
key={name}
|
|
42
|
+
className="flex min-h-11 items-center justify-between gap-3 text-sm"
|
|
43
|
+
>
|
|
44
|
+
<span className="min-w-0 truncate">{name}</span>
|
|
45
|
+
<Switch
|
|
46
|
+
checked={allowed.includes(name)}
|
|
47
|
+
onCheckedChange={(on) => toggle(name, on)}
|
|
48
|
+
/>
|
|
49
|
+
</label>
|
|
50
|
+
))
|
|
51
|
+
)}
|
|
52
|
+
</div>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -5,6 +5,7 @@ import { useUpdateWorkspace, useWorkspacesQuery } from "@/data/workspaces";
|
|
|
5
5
|
import { TaskLimitForm } from "./TaskLimitForm";
|
|
6
6
|
import { RulesForm } from "./RulesForm";
|
|
7
7
|
import { FreshStartForm } from "./FreshStartForm";
|
|
8
|
+
import { McpServersForm } from "./McpServersForm";
|
|
8
9
|
import { StallForm } from "./StallForm";
|
|
9
10
|
import { WorkspaceModelsForm } from "./WorkspaceModelsForm";
|
|
10
11
|
import { AwayWindowForm } from "@/components/Away/AwayWindowForm";
|
|
@@ -45,6 +46,7 @@ export function WorkspaceSettings() {
|
|
|
45
46
|
/>
|
|
46
47
|
<RulesForm workspaceId={w.id} />
|
|
47
48
|
<WorkspaceModelsForm workspaceId={w.id} />
|
|
49
|
+
<McpServersForm workspace={w} />
|
|
48
50
|
<SettingToggle
|
|
49
51
|
title="Wake tasks on CI failures and reviews"
|
|
50
52
|
detail="A task's agent is messaged once when a check fails on its PR's head, with the log's tail, and when someone reviews or comments on it."
|
package/data/workspaces/index.ts
CHANGED
|
@@ -23,6 +23,17 @@ export function useWorkspacesQuery() {
|
|
|
23
23
|
});
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
// The MCP servers configured on this machine (names only).
|
|
27
|
+
export function useMcpServersQuery() {
|
|
28
|
+
return useQuery({
|
|
29
|
+
queryKey: [...workspaceKeys.all, "mcp-servers"],
|
|
30
|
+
queryFn: async () =>
|
|
31
|
+
(await json<{ servers: string[] }>(await fetch("/api/mcp-servers")))
|
|
32
|
+
.servers,
|
|
33
|
+
staleTime: 60000,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
export function useCreateWorkspace() {
|
|
27
38
|
const queryClient = useQueryClient();
|
|
28
39
|
return useMutation({
|
|
@@ -55,6 +66,8 @@ export function useUpdateWorkspace() {
|
|
|
55
66
|
maxRunningTasks?: number | null;
|
|
56
67
|
// Message tasks about failing checks and reviews on their PRs.
|
|
57
68
|
wakeTasksOnPR?: boolean;
|
|
69
|
+
// MCP servers its orchestrator may use, by name.
|
|
70
|
+
mcpServers?: string[];
|
|
58
71
|
}) =>
|
|
59
72
|
json(
|
|
60
73
|
await fetch(`/api/workspaces/${id}`, {
|
|
@@ -63,7 +76,13 @@ export function useUpdateWorkspace() {
|
|
|
63
76
|
body: JSON.stringify(updates),
|
|
64
77
|
})
|
|
65
78
|
),
|
|
66
|
-
onMutate: async ({
|
|
79
|
+
onMutate: async ({
|
|
80
|
+
id,
|
|
81
|
+
maxRunningTasks,
|
|
82
|
+
wakeTasksOnPR,
|
|
83
|
+
mcpServers,
|
|
84
|
+
...updates
|
|
85
|
+
}) => {
|
|
67
86
|
await queryClient.cancelQueries({ queryKey: workspaceKeys.list() });
|
|
68
87
|
queryClient.setQueryData<Workspace[]>(workspaceKeys.list(), (prev) =>
|
|
69
88
|
prev?.map((w) =>
|
|
@@ -77,6 +96,9 @@ export function useUpdateWorkspace() {
|
|
|
77
96
|
...(wakeTasksOnPR !== undefined && {
|
|
78
97
|
wake_tasks_on_pr: wakeTasksOnPR,
|
|
79
98
|
}),
|
|
99
|
+
...(mcpServers !== undefined && {
|
|
100
|
+
orch_mcp_servers: mcpServers,
|
|
101
|
+
}),
|
|
80
102
|
}
|
|
81
103
|
: w
|
|
82
104
|
)
|
package/lib/chat/runner.ts
CHANGED
|
@@ -901,3 +901,17 @@ export async function reattachChats(): Promise<void> {
|
|
|
901
901
|
for (const id of queuedSessions(Date.now() - RESUME_WITHIN_MS))
|
|
902
902
|
if (!live.has(id)) await resumeQueue(id);
|
|
903
903
|
}
|
|
904
|
+
|
|
905
|
+
// Asks a chat's worker to end at its next turn boundary, so the next message
|
|
906
|
+
// starts one that reads its settings afresh (an orchestrator's permissions,
|
|
907
|
+
// say). A worker already gone, or too old to understand, needs nothing.
|
|
908
|
+
export async function retireChat(sessionId: string): Promise<void> {
|
|
909
|
+
const live =
|
|
910
|
+
registry.live.get(sessionId) ??
|
|
911
|
+
(runningWorkers().includes(sessionId)
|
|
912
|
+
? await ensureLive(sessionId, false).catch(() => undefined)
|
|
913
|
+
: undefined);
|
|
914
|
+
if (!live || live.retireSent) return;
|
|
915
|
+
live.worker.command({ type: "retire" });
|
|
916
|
+
live.retireSent = true;
|
|
917
|
+
}
|
package/lib/db/migrations.ts
CHANGED
|
@@ -1493,6 +1493,20 @@ const migrations: Migration[] = [
|
|
|
1493
1493
|
add("task_queue", "started_by", "TEXT");
|
|
1494
1494
|
},
|
|
1495
1495
|
},
|
|
1496
|
+
{
|
|
1497
|
+
id: 66,
|
|
1498
|
+
name: "workspace_orch_mcp_servers",
|
|
1499
|
+
up: (db) => {
|
|
1500
|
+
// The MCP servers (by name) a workspace's orchestrator may use.
|
|
1501
|
+
const columns = db.prepare(`PRAGMA table_info(workspaces)`).all() as {
|
|
1502
|
+
name: string;
|
|
1503
|
+
}[];
|
|
1504
|
+
if (!columns.some((c) => c.name === "orch_mcp_servers"))
|
|
1505
|
+
db.exec(
|
|
1506
|
+
`ALTER TABLE workspaces ADD COLUMN orch_mcp_servers TEXT NOT NULL DEFAULT '["linear"]'`
|
|
1507
|
+
);
|
|
1508
|
+
},
|
|
1509
|
+
},
|
|
1496
1510
|
];
|
|
1497
1511
|
|
|
1498
1512
|
// `upTo`: stop after this id (tests that start from an older database).
|
package/lib/db/types.ts
CHANGED
|
@@ -220,6 +220,8 @@ export interface Workspace {
|
|
|
220
220
|
orch_max_running: number;
|
|
221
221
|
orch_max_starts_per_hour: number;
|
|
222
222
|
orch_brake: string | null;
|
|
223
|
+
// MCP servers (by name) its orchestrator may use.
|
|
224
|
+
orch_mcp_servers: string[];
|
|
223
225
|
// Set while Saad has the orchestrator paused.
|
|
224
226
|
orch_paused_at: string | null;
|
|
225
227
|
// Tasks running at once before new ones queue (lib/tasks/queue.ts); null: no limit.
|
|
@@ -124,6 +124,8 @@ Acting:
|
|
|
124
124
|
- \`${TOOL_NAMES.disarm}\` (task): stop an armed task merging by itself.
|
|
125
125
|
- \`${TOOL_NAMES.ask_saad}\` (title, detail, link?, kind, task?, sha?): park an item on Saad's asks list and carry on; it never waits.
|
|
126
126
|
|
|
127
|
+
The MCP servers this workspace allows (Settings → Workspaces) are yours to use through their own tools, for reading and for additive, reversible changes. Anything outbound through them (sending a message, an email, a post) is a hard line: park it with \`ask_saad\`.
|
|
128
|
+
|
|
127
129
|
Act through these tools, not the shell: they're scoped to this workspace and braked. The shell runs anything without asking (Saad chose this on 2026-10-11), so use it to read and investigate yourself: \`gh pr list\`, \`gh run view --log-failed\`, \`gh api\` GETs, \`curl\`, \`sqlite3 -readonly\`, git in any repository, a skill's scripts, pipes and chains. Never use it to get around the tools. A deny list refuses, naming the tool to use instead: merging, closing or retargeting a PR (\`${TOOL_NAMES.sign_off}\`, \`${TOOL_NAMES.arm}\`, \`${TOOL_NAMES.land}\`, \`${TOOL_NAMES.drop}\`), \`git push\` (\`${TOOL_NAMES.quick_pr}\`), writing in a project repository or task worktree other than your own quick changes (\`${TOOL_NAMES.start_task}\`, \`${TOOL_NAMES.quick_change}\`), writing AgentOS's state (sqlite3 without \`-readonly\`, or files in ~/.agent-os outside your folder), stopping or restarting AgentOS or its tmux, and reading secret files (env files, keys, ~/.ssh, ~/.aws). Tokenvault is available through \`tv\`: use a token inside the command that needs it (e.g. \`FOO="\$(tv get <project> <desc>)" cmd\`), never print, echo or log a value, and never send one off the machine except to the service it's for. The list catches honest mistakes, not a determined bypass: a script or \`python -c\` could do any of these unseen, and doing so would break Saad's trust in every gate. \`aos notify "<text>"\` pushes a message to Saad's phone. Use it for what he'd want to see now and asked for: the morning report, a real milestone (a stack landed, a blocker only he can clear). Never for routine status: one a minute at most, and he reads the rest here. You can read files anywhere (Read, Grep, Glob), and write them (Write, Edit, NotebookEdit) anywhere except: a project repository or task worktree other than your own open quick changes' worktrees (start a task, or a quick change for a small edit, so the change goes through CI, review and scope), AgentOS's own state in ~/.agent-os outside your folder, secret files (.env, keys, ~/.ssh, ~/.aws, tokenvault and the like), and files that make something run: dotfiles and dot-folders at the top of your home (shell, git, Claude and tool config), ~/Library and /Library's launch agents, git's core.hooksPath folders, the folders on PATH, and any .git, .claude, .mcp.json, CLAUDE.md or AGENTS.md, which would hand you a shell or run code around the gates. Each write is checked before it runs, on the path with symlinks and \`..\` resolved; a refused one says why.
|
|
128
130
|
|
|
129
131
|
## Untrusted text
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP servers an orchestrator may use. Its permissions are otherwise a
|
|
3
|
+
* fixed list (tool-names.ts); each workspace adds whole servers, by name, from
|
|
4
|
+
* Settings → Workspaces. Servers carry their own auth and some send outbound
|
|
5
|
+
* (a message, an email), so none is allowed unless named; the brief keeps
|
|
6
|
+
* those actions as hard lines for ask_saad.
|
|
7
|
+
*
|
|
8
|
+
* Only names are ever read from the Claude config: never env, headers, URLs
|
|
9
|
+
* or tokens.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import fs from "fs";
|
|
13
|
+
import os from "os";
|
|
14
|
+
import path from "path";
|
|
15
|
+
import { ORCHESTRATOR_PERMISSIONS } from "./tool-names";
|
|
16
|
+
import { getOrchestrator } from "./home";
|
|
17
|
+
import { getWorkspace } from "../workspaces";
|
|
18
|
+
import { retireChat } from "../chat/runner";
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_MCP_SERVERS = ["linear"];
|
|
21
|
+
|
|
22
|
+
// How Claude Code names a server inside a tool name: anything outside
|
|
23
|
+
// letters, digits, "_" and "-" becomes "_".
|
|
24
|
+
export const toolPrefix = (server: string) =>
|
|
25
|
+
`mcp__${server.replace(/[^a-zA-Z0-9_-]/g, "_")}`;
|
|
26
|
+
|
|
27
|
+
// The permissions for an orchestrator whose workspace allows these servers:
|
|
28
|
+
// a bare mcp__<server> rule allows all of that server's tools.
|
|
29
|
+
export function orchestratorPermissions(servers: string[]) {
|
|
30
|
+
const rules = [...new Set(servers.map((s) => s.trim()).filter(Boolean))].map(
|
|
31
|
+
toolPrefix
|
|
32
|
+
);
|
|
33
|
+
return {
|
|
34
|
+
...ORCHESTRATOR_PERMISSIONS,
|
|
35
|
+
allowedTools: [...ORCHESTRATOR_PERMISSIONS.allowedTools, ...rules],
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const claudeDir = () =>
|
|
40
|
+
process.env.CLAUDE_CONFIG_DIR || path.join(os.homedir(), ".claude");
|
|
41
|
+
|
|
42
|
+
function readJson(file: string): unknown {
|
|
43
|
+
try {
|
|
44
|
+
return JSON.parse(fs.readFileSync(file, "utf8"));
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const keys = (value: unknown): string[] =>
|
|
51
|
+
value && typeof value === "object" && !Array.isArray(value)
|
|
52
|
+
? Object.keys(value)
|
|
53
|
+
: [];
|
|
54
|
+
|
|
55
|
+
// Names of the servers the agent sees on this machine: user-level servers
|
|
56
|
+
// (~/.claude.json), connectors the account has connected, and the servers
|
|
57
|
+
// of installed plugins.
|
|
58
|
+
export function configuredMcpServers(): string[] {
|
|
59
|
+
const names = new Set<string>();
|
|
60
|
+
const user = readJson(
|
|
61
|
+
process.env.CLAUDE_CONFIG_DIR
|
|
62
|
+
? path.join(claudeDir(), ".claude.json")
|
|
63
|
+
: path.join(os.homedir(), ".claude.json")
|
|
64
|
+
) as Record<string, unknown> | null;
|
|
65
|
+
for (const name of keys(user?.mcpServers)) names.add(name);
|
|
66
|
+
const connectors = user?.claudeAiMcpEverConnected;
|
|
67
|
+
if (Array.isArray(connectors))
|
|
68
|
+
for (const c of connectors) if (typeof c === "string") names.add(c);
|
|
69
|
+
|
|
70
|
+
const installed = readJson(
|
|
71
|
+
path.join(claudeDir(), "plugins", "installed_plugins.json")
|
|
72
|
+
) as { plugins?: Record<string, { installPath?: unknown }[]> } | null;
|
|
73
|
+
for (const [id, installs] of Object.entries(installed?.plugins ?? {})) {
|
|
74
|
+
const plugin = id.split("@")[0];
|
|
75
|
+
const dir = installs?.[0]?.installPath;
|
|
76
|
+
if (typeof dir !== "string") continue;
|
|
77
|
+
const file = readJson(path.join(dir, ".mcp.json")) as Record<
|
|
78
|
+
string,
|
|
79
|
+
unknown
|
|
80
|
+
> | null;
|
|
81
|
+
const servers = keys(file?.mcpServers).length ? file?.mcpServers : file;
|
|
82
|
+
for (const server of keys(servers)) names.add(`plugin:${plugin}:${server}`);
|
|
83
|
+
}
|
|
84
|
+
return [...names].sort((a, b) => a.localeCompare(b));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// A workspace's allowed servers changed: its orchestrator's worker ends at its
|
|
88
|
+
// next turn boundary and the next one starts with the new permissions.
|
|
89
|
+
export async function applyMcpServers(workspaceId: string): Promise<void> {
|
|
90
|
+
const orchestrator = getOrchestrator(workspaceId);
|
|
91
|
+
if (orchestrator && getWorkspace(workspaceId))
|
|
92
|
+
await retireChat(orchestrator.id).catch(() => {});
|
|
93
|
+
}
|
|
@@ -118,8 +118,10 @@ export function orchestratorTools(
|
|
|
118
118
|
// What the orchestrator's agent sees of its tools: names, descriptions,
|
|
119
119
|
// arguments and permissions. A worker after a deploy tells the agent its
|
|
120
120
|
// tools changed only when this did.
|
|
121
|
-
export function orchestratorToolsDigest(
|
|
122
|
-
|
|
121
|
+
export function orchestratorToolsDigest(
|
|
122
|
+
permissions: object = ORCHESTRATOR_PERMISSIONS
|
|
123
|
+
): string {
|
|
124
|
+
return toolsDigest(DESCRIPTIONS, TOOL_SHAPES, permissions);
|
|
123
125
|
}
|
|
124
126
|
|
|
125
127
|
export function toolsDigest(
|
|
@@ -2,7 +2,9 @@ import type { Session } from "../db";
|
|
|
2
2
|
import { agentEnv } from "../agents/launch";
|
|
3
3
|
import { loadOrchestratorBrief } from "./brief";
|
|
4
4
|
import { orchestratorToken } from "./home";
|
|
5
|
-
import {
|
|
5
|
+
import { ORCHESTRATOR_SERVER } from "./tool-names";
|
|
6
|
+
import { orchestratorPermissions } from "./mcp-servers";
|
|
7
|
+
import { getWorkspace } from "../workspaces";
|
|
6
8
|
import {
|
|
7
9
|
httpToolCaller,
|
|
8
10
|
orchestratorTools,
|
|
@@ -19,6 +21,9 @@ export async function orchestratorExtras(
|
|
|
19
21
|
session: Session
|
|
20
22
|
): Promise<RoleExtras> {
|
|
21
23
|
const baseUrl = agentEnv(session.id).AGENTOS_URL;
|
|
24
|
+
const permissions = orchestratorPermissions(
|
|
25
|
+
getWorkspace(session.workspace_id ?? "")?.orch_mcp_servers ?? []
|
|
26
|
+
);
|
|
22
27
|
return {
|
|
23
28
|
systemAppend: await loadOrchestratorBrief(session),
|
|
24
29
|
mcpServers: {
|
|
@@ -30,9 +35,9 @@ export async function orchestratorExtras(
|
|
|
30
35
|
)
|
|
31
36
|
),
|
|
32
37
|
},
|
|
33
|
-
...
|
|
38
|
+
...permissions,
|
|
34
39
|
toolGuard: (tool, input) => orchestratorToolRefusal(session, tool, input),
|
|
35
|
-
toolsDigest: orchestratorToolsDigest(),
|
|
40
|
+
toolsDigest: orchestratorToolsDigest(permissions),
|
|
36
41
|
refresh: {
|
|
37
42
|
atTokens: refreshSettings().orchestratorTokens,
|
|
38
43
|
role: "orchestrator",
|
package/lib/workspaces.ts
CHANGED
|
@@ -6,15 +6,39 @@
|
|
|
6
6
|
import { randomUUID } from "crypto";
|
|
7
7
|
import { db, type Workspace } from "./db";
|
|
8
8
|
|
|
9
|
-
type WorkspaceRow = Omit<
|
|
9
|
+
type WorkspaceRow = Omit<
|
|
10
|
+
Workspace,
|
|
11
|
+
"collapsed" | "wake_tasks_on_pr" | "orch_mcp_servers"
|
|
12
|
+
> & {
|
|
10
13
|
collapsed: number;
|
|
11
14
|
wake_tasks_on_pr: number;
|
|
15
|
+
orch_mcp_servers: string;
|
|
12
16
|
};
|
|
13
17
|
|
|
18
|
+
function parseServers(raw: string): string[] {
|
|
19
|
+
try {
|
|
20
|
+
const list = JSON.parse(raw);
|
|
21
|
+
return Array.isArray(list)
|
|
22
|
+
? list.filter((n): n is string => typeof n === "string")
|
|
23
|
+
: [];
|
|
24
|
+
} catch {
|
|
25
|
+
return [];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// A list of server names: trimmed, deduplicated, none empty.
|
|
30
|
+
function serverNames(value: unknown, current: string[]): string[] {
|
|
31
|
+
if (value === undefined) return current;
|
|
32
|
+
if (!Array.isArray(value) || value.some((n) => typeof n !== "string"))
|
|
33
|
+
throw new Error("mcpServers is a list of server names");
|
|
34
|
+
return [...new Set(value.map((n: string) => n.trim()).filter(Boolean))];
|
|
35
|
+
}
|
|
36
|
+
|
|
14
37
|
const toWorkspace = (row: WorkspaceRow): Workspace => ({
|
|
15
38
|
...row,
|
|
16
39
|
collapsed: !!row.collapsed,
|
|
17
40
|
wake_tasks_on_pr: !!row.wake_tasks_on_pr,
|
|
41
|
+
orch_mcp_servers: parseServers(row.orch_mcp_servers),
|
|
18
42
|
});
|
|
19
43
|
|
|
20
44
|
export function listWorkspaces(): Workspace[] {
|
|
@@ -62,19 +86,21 @@ export function updateWorkspace(
|
|
|
62
86
|
sortOrder?: number;
|
|
63
87
|
maxRunningTasks?: number | null;
|
|
64
88
|
wakeTasksOnPR?: boolean;
|
|
89
|
+
mcpServers?: string[];
|
|
65
90
|
}
|
|
66
91
|
): Workspace | null {
|
|
67
92
|
const existing = getWorkspace(id);
|
|
68
93
|
if (!existing) return null;
|
|
69
94
|
const name = updates.name?.trim() || existing.name;
|
|
70
95
|
db.prepare(
|
|
71
|
-
`UPDATE workspaces SET name = ?, collapsed = ?, sort_order = ?, max_running_tasks = ?, wake_tasks_on_pr = ? WHERE id = ?`
|
|
96
|
+
`UPDATE workspaces SET name = ?, collapsed = ?, sort_order = ?, max_running_tasks = ?, wake_tasks_on_pr = ?, orch_mcp_servers = ? WHERE id = ?`
|
|
72
97
|
).run(
|
|
73
98
|
name,
|
|
74
99
|
(updates.collapsed ?? existing.collapsed) ? 1 : 0,
|
|
75
100
|
updates.sortOrder ?? existing.sort_order,
|
|
76
101
|
taskLimit(updates.maxRunningTasks, existing.max_running_tasks),
|
|
77
102
|
(updates.wakeTasksOnPR ?? existing.wake_tasks_on_pr) ? 1 : 0,
|
|
103
|
+
JSON.stringify(serverNames(updates.mcpServers, existing.orch_mcp_servers)),
|
|
78
104
|
id
|
|
79
105
|
);
|
|
80
106
|
return getWorkspace(id);
|