@saadnvd1/agent-os 0.3.662 → 0.3.671
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/app/api/audits/[kind]/[id]/route.test.ts +2 -2
- package/app/api/mcp-auth/callback/route.test.ts +42 -0
- package/app/api/mcp-auth/callback/route.ts +56 -0
- package/app/api/projects/init/route.test.ts +2 -4
- package/app/api/sessions/[id]/mcp-auth/route.test.ts +86 -0
- package/app/api/sessions/[id]/mcp-auth/route.ts +58 -0
- package/app/api/sessions/[id]/route.ts +3 -3
- package/app/api/shares/route.test.ts +3 -4
- package/app/api/workspaces/[id]/route.ts +9 -1
- package/app/api/worktree-sweep/route.test.ts +40 -1
- package/app/api/worktree-sweep/route.ts +45 -6
- package/components/Chat/ChatPanel.tsx +6 -3
- package/components/Chat/McpServers.tsx +61 -24
- package/components/Chat/McpSignIn.tsx +122 -0
- package/components/Chat/session.ts +8 -0
- package/components/Health/SweepRuns.tsx +77 -1
- package/components/Workspaces/WorkspaceSettings.tsx +6 -0
- package/data/chat/mcpAuth.ts +42 -0
- package/data/workspaces/index.ts +6 -0
- package/data/worktree-sweep/index.ts +15 -0
- package/lib/agentos-home.ts +11 -0
- package/lib/agents/launch.ts +3 -7
- package/lib/artifacts/store.ts +2 -5
- package/lib/chat/driver.ts +24 -2
- package/lib/chat/drivers/claude-approvals.ts +7 -0
- package/lib/chat/drivers/claude-mcp-auth.ts +82 -0
- package/lib/chat/drivers/claude.ts +7 -0
- package/lib/chat/drivers/pi-approvals.ts +2 -2
- package/lib/chat/events.ts +12 -0
- package/lib/chat/mcp-auth.ts +193 -0
- package/lib/chat/registry.ts +2 -0
- package/lib/chat/runner.ts +18 -0
- package/lib/chat/worker/client.ts +35 -1
- package/lib/chat/worker/host.ts +9 -0
- package/lib/chat/worker/main.ts +1 -1
- package/lib/chat/worker/mcp-auth.ts +119 -0
- package/lib/chat/worker/protocol.ts +21 -2
- package/lib/connect/config.ts +2 -3
- package/lib/db/backup.ts +2 -2
- package/lib/db/migrations/20261018T1600_health_boot_deploys.ts +13 -0
- package/lib/db/migrations/20261018T1730_mcp_sign_ins.ts +22 -0
- package/lib/db/migrations/20261018T2100_orch_full_access.ts +16 -0
- package/lib/db/types.ts +3 -0
- package/lib/dev-instance/processInfo.ts +48 -27
- package/lib/dev-instance/startDevServer.ts +1 -1
- package/lib/dev-instance/stopDevServer.ts +20 -17
- package/lib/dev-servers.ts +2 -1
- package/lib/done/index.ts +2 -2
- package/lib/done/testing.ts +2 -2
- package/lib/env-setup.ts +8 -1
- package/lib/health/boots.ts +31 -0
- package/lib/health/ci.ts +23 -2
- package/lib/health/detectors.ts +2 -1
- package/lib/health/evidence.ts +1 -1
- package/lib/health/facts.ts +3 -3
- package/lib/health/fixtures.ts +1 -0
- package/lib/health/tick.ts +6 -1
- package/lib/health/types.ts +2 -0
- package/lib/orchestrator/armed.test-helpers.ts +3 -2
- package/lib/orchestrator/brief.ts +14 -9
- package/lib/orchestrator/external-gates.ts +62 -1
- package/lib/orchestrator/full-access.ts +21 -0
- package/lib/orchestrator/home.ts +2 -5
- package/lib/orchestrator/quick.ts +47 -4
- package/lib/orchestrator/serve.ts +11 -2
- package/lib/orchestrator/shell-guard.ts +24 -0
- package/lib/orchestrator/signoff.test-helpers.ts +3 -2
- package/lib/orchestrator/sweep-arm.ts +180 -0
- package/lib/orchestrator/tool-names.ts +4 -1
- package/lib/orchestrator/tool-schemas.ts +33 -1
- package/lib/orchestrator/tools.ts +6 -2
- package/lib/orchestrator/write-guard.ts +37 -9
- package/lib/plans/features.ts +2 -2
- package/lib/program-status/claude-hooks.ts +2 -2
- package/lib/program-status/tap.ts +2 -2
- package/lib/project-config/brief.ts +3 -1
- package/lib/project-config/database.ts +77 -16
- package/lib/project-config/index.ts +108 -21
- package/lib/project-config/schema.ts +10 -4
- package/lib/project-config/session.ts +27 -6
- package/lib/security/net.ts +7 -4
- package/lib/security/tailnet-https.ts +2 -2
- package/lib/session-artifacts/collect.ts +18 -1
- package/lib/session-artifacts/keep.ts +2 -4
- package/lib/session-audit/store.ts +2 -5
- package/lib/session-logs/folder.ts +2 -3
- package/lib/sessions/launch.ts +2 -2
- package/lib/sessions/leftovers.ts +61 -0
- package/lib/simulator/agent-device.ts +2 -2
- package/lib/tasks/finish.ts +2 -2
- package/lib/tasks/import.ts +3 -5
- package/lib/tasks/move-testing.ts +2 -2
- package/lib/tasks/move.ts +2 -2
- package/lib/tasks/moved-away.ts +2 -0
- package/lib/tasks/state.ts +7 -3
- package/lib/tasks/ui-brief.ts +2 -2
- package/lib/test-temp.ts +30 -0
- package/lib/workspaces.ts +6 -2
- package/lib/worktree-deps/fastcopy.ts +2 -2
- package/lib/worktree-deps/spare.ts +2 -2
- package/lib/worktree-sweep/caches.ts +129 -0
- package/lib/worktree-sweep/databases.ts +222 -0
- package/lib/worktree-sweep/du.ts +13 -0
- package/lib/worktree-sweep/extras.ts +196 -0
- package/lib/worktree-sweep/findOrphans.ts +19 -8
- package/lib/worktree-sweep/report.ts +86 -0
- package/lib/worktree-sweep/runSweep.ts +46 -13
- package/lib/worktree-sweep/settings.ts +24 -0
- package/lib/worktree-sweep/state.ts +5 -0
- package/lib/worktree-sweep/store.ts +11 -0
- package/lib/worktree-sweep/testTemp.ts +113 -0
- package/lib/worktree-sweep/types.ts +46 -1
- package/lib/worktrees.ts +3 -0
- package/package.json +2 -1
- package/scripts/autodeploy.test.ts +2 -2
- package/scripts/check-bundle.test.ts +2 -2
- package/scripts/disk-dry-run.ts +57 -0
- package/scripts/hooks/banned-terms.test.ts +3 -2
- package/scripts/hooks/staged-secrets.test.ts +2 -2
- package/scripts/lib/build-swap.test.ts +2 -3
- package/scripts/lib/wsl.test.ts +2 -3
- package/scripts/update-run.test.ts +2 -2
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { ExternalLink } from "lucide-react";
|
|
5
|
+
import type { useMcpSignIn } from "@/data/chat/mcpAuth";
|
|
6
|
+
|
|
7
|
+
const LINK =
|
|
8
|
+
"bg-primary text-primary-foreground inline-flex h-11 items-center justify-center gap-2 rounded-lg px-4 text-sm font-medium md:h-9";
|
|
9
|
+
const GHOST =
|
|
10
|
+
"text-muted-foreground hover:text-foreground h-11 rounded-lg px-3 text-xs md:h-9";
|
|
11
|
+
|
|
12
|
+
// What happens after Sign in is pressed on one server: the link to open, and
|
|
13
|
+
// what to do if the provider can't return to AgentOS by itself. The list row
|
|
14
|
+
// turns connected on its own, over the chat socket, when it's done.
|
|
15
|
+
export function McpSignIn({
|
|
16
|
+
flow: { start, finish, recheck },
|
|
17
|
+
server,
|
|
18
|
+
onClose,
|
|
19
|
+
}: {
|
|
20
|
+
// Started by the Sign in press (not on mount: a dev double-mount would
|
|
21
|
+
// drop the request's result).
|
|
22
|
+
flow: ReturnType<typeof useMcpSignIn>;
|
|
23
|
+
server: string;
|
|
24
|
+
onClose: () => void;
|
|
25
|
+
}) {
|
|
26
|
+
const [pasted, setPasted] = useState("");
|
|
27
|
+
|
|
28
|
+
const mode = start.data?.mode;
|
|
29
|
+
const authUrl = start.data?.authUrl;
|
|
30
|
+
const error = start.error ?? finish.error ?? recheck.error;
|
|
31
|
+
const busy = start.isPending || finish.isPending || recheck.isPending;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className="space-y-2 pt-1 pr-2 pb-2 pl-6.5 text-xs">
|
|
35
|
+
{start.isPending && (
|
|
36
|
+
<p className="text-muted-foreground">Starting sign-in…</p>
|
|
37
|
+
)}
|
|
38
|
+
{mode === "done" && (
|
|
39
|
+
<p className="text-muted-foreground">
|
|
40
|
+
Already signed in; reconnecting.
|
|
41
|
+
</p>
|
|
42
|
+
)}
|
|
43
|
+
{(mode === "callback" || mode === "paste" || mode === "external") &&
|
|
44
|
+
authUrl && (
|
|
45
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
46
|
+
<a
|
|
47
|
+
href={authUrl}
|
|
48
|
+
target="_blank"
|
|
49
|
+
rel="noopener noreferrer"
|
|
50
|
+
className={LINK}
|
|
51
|
+
>
|
|
52
|
+
{mode === "external" ? "Open claude.ai" : "Open sign-in page"}
|
|
53
|
+
<ExternalLink className="size-4" />
|
|
54
|
+
</a>
|
|
55
|
+
{mode === "external" && (
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
disabled={busy}
|
|
59
|
+
onClick={() => recheck.mutate()}
|
|
60
|
+
className={GHOST}
|
|
61
|
+
>
|
|
62
|
+
Check again
|
|
63
|
+
</button>
|
|
64
|
+
)}
|
|
65
|
+
</div>
|
|
66
|
+
)}
|
|
67
|
+
{mode === "callback" && (
|
|
68
|
+
<p className="text-muted-foreground">
|
|
69
|
+
Approve it there. This list updates by itself.
|
|
70
|
+
</p>
|
|
71
|
+
)}
|
|
72
|
+
{mode === "external" && (
|
|
73
|
+
<p className="text-muted-foreground">
|
|
74
|
+
This server signs in on claude.ai. Finish there, then come back.
|
|
75
|
+
</p>
|
|
76
|
+
)}
|
|
77
|
+
{mode === "paste" && (
|
|
78
|
+
<>
|
|
79
|
+
<p className="text-muted-foreground">
|
|
80
|
+
This server only sends the sign-in back to the machine running the
|
|
81
|
+
session. On this device the page you land on won't load: copy
|
|
82
|
+
its address and paste it here.
|
|
83
|
+
</p>
|
|
84
|
+
<form
|
|
85
|
+
className="flex items-center gap-2"
|
|
86
|
+
onSubmit={(e) => {
|
|
87
|
+
e.preventDefault();
|
|
88
|
+
if (pasted.trim()) finish.mutate(pasted.trim());
|
|
89
|
+
}}
|
|
90
|
+
>
|
|
91
|
+
<input
|
|
92
|
+
value={pasted}
|
|
93
|
+
onChange={(e) => setPasted(e.target.value)}
|
|
94
|
+
inputMode="url"
|
|
95
|
+
autoComplete="off"
|
|
96
|
+
autoCapitalize="off"
|
|
97
|
+
spellCheck={false}
|
|
98
|
+
placeholder="Address of the page after approving"
|
|
99
|
+
aria-label={`Address after signing in to ${server}`}
|
|
100
|
+
className="bg-background hairline h-11 min-w-0 flex-1 rounded-lg px-3 text-base md:h-9 md:text-xs"
|
|
101
|
+
/>
|
|
102
|
+
<button
|
|
103
|
+
type="submit"
|
|
104
|
+
disabled={busy || !pasted.trim()}
|
|
105
|
+
className="bg-secondary text-secondary-foreground hairline h-11 shrink-0 rounded-lg px-3 text-xs font-medium disabled:opacity-50 md:h-9"
|
|
106
|
+
>
|
|
107
|
+
Finish
|
|
108
|
+
</button>
|
|
109
|
+
</form>
|
|
110
|
+
</>
|
|
111
|
+
)}
|
|
112
|
+
{error && (
|
|
113
|
+
<p className="text-destructive whitespace-pre-wrap" role="alert">
|
|
114
|
+
{error.message}
|
|
115
|
+
</p>
|
|
116
|
+
)}
|
|
117
|
+
<button type="button" onClick={onClose} className={GHOST}>
|
|
118
|
+
{error ? "Close" : "Cancel"}
|
|
119
|
+
</button>
|
|
120
|
+
</div>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { createContext, useContext } from "react";
|
|
4
|
+
|
|
5
|
+
// The session this chat belongs to, for rows that call its API (a context,
|
|
6
|
+
// not a prop through every item).
|
|
7
|
+
export const ChatSessionId = createContext<string | null>(null);
|
|
8
|
+
export const useChatSessionId = () => useContext(ChatSessionId);
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { Button } from "@/components/ui/button";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useRunSweep,
|
|
6
|
+
useSetReaperArmed,
|
|
7
|
+
useWorktreeSweep,
|
|
8
|
+
} from "@/data/worktree-sweep";
|
|
5
9
|
import { compactTimeAgo, fromSqliteTime } from "@/lib/session-meta";
|
|
6
10
|
import { formatBytes } from "@/lib/share/view";
|
|
7
11
|
import type { SweepRun } from "@/lib/worktree-sweep/types";
|
|
@@ -10,8 +14,35 @@ const KIND: Record<string, string> = {
|
|
|
10
14
|
settled: "settled session",
|
|
11
15
|
orphan: "orphan folder",
|
|
12
16
|
"test-leak": "test leftover",
|
|
17
|
+
database: "task database",
|
|
18
|
+
spare: "dependency spare",
|
|
19
|
+
temp: "temp folder",
|
|
20
|
+
"test-temp": "test temp folder",
|
|
21
|
+
"derived-data": "Xcode build folder",
|
|
13
22
|
};
|
|
14
23
|
|
|
24
|
+
// "18.2 GB in 3,876 worktrees · 12 task databases (270 MB) · ..." from the
|
|
25
|
+
// latest run that measured, and what the last real run freed.
|
|
26
|
+
export function diskSummary(runs: SweepRun[]): string | null {
|
|
27
|
+
const totals = runs.find((r) => r.details?.totals)?.details?.totals;
|
|
28
|
+
if (!totals) return null;
|
|
29
|
+
const part = (n: number, bytes: number, what: string) =>
|
|
30
|
+
`${n.toLocaleString()} ${what}${bytes ? ` (${formatBytes(bytes)})` : ""}`;
|
|
31
|
+
const last = runs.find((r) => !r.dryRun && r.finishedAt);
|
|
32
|
+
return [
|
|
33
|
+
`Task leftovers: ${part(totals.worktrees.count, totals.worktrees.bytes, "worktrees")}`,
|
|
34
|
+
part(
|
|
35
|
+
totals.databases.count,
|
|
36
|
+
totals.databases.bytes,
|
|
37
|
+
"stale task databases"
|
|
38
|
+
),
|
|
39
|
+
part(totals.spares.count, totals.spares.bytes, "dependency spares"),
|
|
40
|
+
last
|
|
41
|
+
? `last clean-up freed ${formatBytes(last.freedBytes)}, ${compactTimeAgo(fromSqliteTime(last.finishedAt!))}`
|
|
42
|
+
: "no clean-up has run yet",
|
|
43
|
+
].join(" · ");
|
|
44
|
+
}
|
|
45
|
+
|
|
15
46
|
const name = (p: string) => p.split("/").pop();
|
|
16
47
|
|
|
17
48
|
function Run({ run }: { run: SweepRun }) {
|
|
@@ -51,6 +82,22 @@ function Run({ run }: { run: SweepRun }) {
|
|
|
51
82
|
</ul>
|
|
52
83
|
</div>
|
|
53
84
|
)}
|
|
85
|
+
{(run.details?.pending?.length ?? 0) > 0 && (
|
|
86
|
+
<div className="mt-2 space-y-1 pb-2">
|
|
87
|
+
<p className="text-sm font-medium">
|
|
88
|
+
Waiting for the full clean-up to be turned on (
|
|
89
|
+
{run.details?.pendingTotal?.count ?? 0},{" "}
|
|
90
|
+
{formatBytes(run.details?.pendingTotal?.bytes ?? 0)})
|
|
91
|
+
</p>
|
|
92
|
+
<ul className="text-muted-foreground space-y-1 text-xs">
|
|
93
|
+
{run.details?.pending?.slice(0, 100).map((p) => (
|
|
94
|
+
<li key={p.path} className="break-all">
|
|
95
|
+
{name(p.path)} ({KIND[p.kind]}, {formatBytes(p.bytes)})
|
|
96
|
+
</li>
|
|
97
|
+
))}
|
|
98
|
+
</ul>
|
|
99
|
+
</div>
|
|
100
|
+
)}
|
|
54
101
|
{removed.length > 0 && (
|
|
55
102
|
<div className="mt-2 space-y-1 pb-2">
|
|
56
103
|
<p className="text-sm font-medium">Removed</p>
|
|
@@ -72,8 +119,37 @@ function Run({ run }: { run: SweepRun }) {
|
|
|
72
119
|
export function SweepRuns() {
|
|
73
120
|
const { data, isPending, error } = useWorktreeSweep();
|
|
74
121
|
const run = useRunSweep();
|
|
122
|
+
const arm = useSetReaperArmed();
|
|
123
|
+
const summary = data ? diskSummary(data.runs) : null;
|
|
75
124
|
return (
|
|
76
125
|
<div className="space-y-3">
|
|
126
|
+
{summary && (
|
|
127
|
+
<p
|
|
128
|
+
className="text-sm font-medium tabular-nums"
|
|
129
|
+
data-testid="disk-summary"
|
|
130
|
+
>
|
|
131
|
+
{summary}
|
|
132
|
+
</p>
|
|
133
|
+
)}
|
|
134
|
+
{data && (
|
|
135
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
136
|
+
<p className="text-muted-foreground min-w-0 flex-1 text-sm">
|
|
137
|
+
Full clean-up is {data.armed ? "on" : "off"}: test leftovers, stale
|
|
138
|
+
task databases, spares, temp folders and build caches{" "}
|
|
139
|
+
{data.armed
|
|
140
|
+
? "are removed with each run."
|
|
141
|
+
: "are only listed until you turn it on. Read a dry run first."}
|
|
142
|
+
</p>
|
|
143
|
+
<Button
|
|
144
|
+
variant="outline"
|
|
145
|
+
className="min-h-11 md:min-h-8"
|
|
146
|
+
disabled={arm.isPending}
|
|
147
|
+
onClick={() => arm.mutate(!data.armed)}
|
|
148
|
+
>
|
|
149
|
+
{data.armed ? "Turn off" : "Turn on"}
|
|
150
|
+
</Button>
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
77
153
|
<div className="flex flex-wrap items-center gap-2">
|
|
78
154
|
<p className="text-muted-foreground min-w-0 flex-1 text-sm">
|
|
79
155
|
Settled worktrees older than {data?.days ?? data?.defaultDays ?? 7}{" "}
|
|
@@ -55,6 +55,12 @@ export function WorkspaceSettings() {
|
|
|
55
55
|
checked={w.wake_tasks_on_pr}
|
|
56
56
|
onChange={(on) => update.mutate({ id: w.id, wakeTasksOnPR: on })}
|
|
57
57
|
/>
|
|
58
|
+
<SettingToggle
|
|
59
|
+
title="Orchestrator full access"
|
|
60
|
+
detail="Its shell and file writes aren't checked: it can push, commit, edit any repository or file on this machine, and merge a PR directly. Off: the deny list, write limits, quick-change cap and merge-only-through-the-gates apply."
|
|
61
|
+
checked={w.orch_full_access}
|
|
62
|
+
onChange={(on) => update.mutate({ id: w.id, orchFullAccess: on })}
|
|
63
|
+
/>
|
|
58
64
|
</section>
|
|
59
65
|
))}
|
|
60
66
|
<FreshStartForm />
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useMutation } from "@tanstack/react-query";
|
|
2
|
+
import type { McpSignInStart } from "@/lib/chat/events";
|
|
3
|
+
|
|
4
|
+
type Body = {
|
|
5
|
+
action: "start" | "submit" | "reconnect";
|
|
6
|
+
server: string;
|
|
7
|
+
itemId: string;
|
|
8
|
+
url?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
async function post<T>(sessionId: string, body: Body): Promise<T> {
|
|
12
|
+
const res = await fetch(`/api/sessions/${sessionId}/mcp-auth`, {
|
|
13
|
+
method: "POST",
|
|
14
|
+
headers: { "Content-Type": "application/json" },
|
|
15
|
+
body: JSON.stringify(body),
|
|
16
|
+
});
|
|
17
|
+
const data = await res.json().catch(() => ({}));
|
|
18
|
+
if (!res.ok) throw new Error(data.error || `Request failed (${res.status})`);
|
|
19
|
+
return data as T;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Sign in to one MCP server of a chat's /mcp list. The list itself updates
|
|
23
|
+
// over the chat socket once the server is back.
|
|
24
|
+
export function useMcpSignIn(
|
|
25
|
+
sessionId: string,
|
|
26
|
+
itemId: string,
|
|
27
|
+
server: string
|
|
28
|
+
) {
|
|
29
|
+
const start = useMutation({
|
|
30
|
+
mutationFn: () =>
|
|
31
|
+
post<McpSignInStart>(sessionId, { action: "start", server, itemId }),
|
|
32
|
+
});
|
|
33
|
+
const finish = useMutation({
|
|
34
|
+
mutationFn: (url: string) =>
|
|
35
|
+
post<{ ok: true }>(sessionId, { action: "submit", server, itemId, url }),
|
|
36
|
+
});
|
|
37
|
+
const recheck = useMutation({
|
|
38
|
+
mutationFn: () =>
|
|
39
|
+
post<{ ok: true }>(sessionId, { action: "reconnect", server, itemId }),
|
|
40
|
+
});
|
|
41
|
+
return { start, finish, recheck };
|
|
42
|
+
}
|
package/data/workspaces/index.ts
CHANGED
|
@@ -66,6 +66,8 @@ export function useUpdateWorkspace() {
|
|
|
66
66
|
maxRunningTasks?: number | null;
|
|
67
67
|
// Message tasks about failing checks and reviews on their PRs.
|
|
68
68
|
wakeTasksOnPR?: boolean;
|
|
69
|
+
// Its orchestrator runs without guards.
|
|
70
|
+
orchFullAccess?: boolean;
|
|
69
71
|
// MCP servers its orchestrator may use, by name.
|
|
70
72
|
mcpServers?: string[];
|
|
71
73
|
}) =>
|
|
@@ -80,6 +82,7 @@ export function useUpdateWorkspace() {
|
|
|
80
82
|
id,
|
|
81
83
|
maxRunningTasks,
|
|
82
84
|
wakeTasksOnPR,
|
|
85
|
+
orchFullAccess,
|
|
83
86
|
mcpServers,
|
|
84
87
|
...updates
|
|
85
88
|
}) => {
|
|
@@ -96,6 +99,9 @@ export function useUpdateWorkspace() {
|
|
|
96
99
|
...(wakeTasksOnPR !== undefined && {
|
|
97
100
|
wake_tasks_on_pr: wakeTasksOnPR,
|
|
98
101
|
}),
|
|
102
|
+
...(orchFullAccess !== undefined && {
|
|
103
|
+
orch_full_access: orchFullAccess,
|
|
104
|
+
}),
|
|
99
105
|
...(mcpServers !== undefined && {
|
|
100
106
|
orch_mcp_servers: mcpServers,
|
|
101
107
|
}),
|
|
@@ -33,6 +33,21 @@ export function useSetSweepDays() {
|
|
|
33
33
|
});
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export function useSetReaperArmed() {
|
|
37
|
+
const queryClient = useQueryClient();
|
|
38
|
+
return useMutation({
|
|
39
|
+
mutationFn: async (armed: boolean) =>
|
|
40
|
+
json<SweepState>(
|
|
41
|
+
await fetch("/api/worktree-sweep", {
|
|
42
|
+
method: "PUT",
|
|
43
|
+
headers: { "Content-Type": "application/json" },
|
|
44
|
+
body: JSON.stringify({ armed }),
|
|
45
|
+
})
|
|
46
|
+
),
|
|
47
|
+
onSuccess: (data) => queryClient.setQueryData(sweepKeys.all, data),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
export function useRunSweep() {
|
|
37
52
|
const queryClient = useQueryClient();
|
|
38
53
|
return useMutation({
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { refuseRealStateUnderTest } from "./test-home-guard";
|
|
4
|
+
|
|
5
|
+
// Every path under ~/.agent-os is built here, so a test run that resolves one
|
|
6
|
+
// to the real home throws instead of leaving files in the owner's state.
|
|
7
|
+
export function agentosHome(...parts: string[]): string {
|
|
8
|
+
return refuseRealStateUnderTest(
|
|
9
|
+
path.join(os.homedir(), ".agent-os", ...parts)
|
|
10
|
+
);
|
|
11
|
+
}
|
package/lib/agents/launch.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { agentosHome } from "../agentos-home";
|
|
1
2
|
import fs from "fs";
|
|
2
|
-
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { execFile } from "child_process";
|
|
5
5
|
import { promisify } from "util";
|
|
@@ -22,12 +22,8 @@ import {
|
|
|
22
22
|
} from "../project-config/session";
|
|
23
23
|
|
|
24
24
|
const execFileAsync = promisify(execFile);
|
|
25
|
-
const PROMPTS_DIR =
|
|
26
|
-
export const BUS_BRIEF_FILE =
|
|
27
|
-
os.homedir(),
|
|
28
|
-
".agent-os",
|
|
29
|
-
"bus-brief.md"
|
|
30
|
-
);
|
|
25
|
+
const PROMPTS_DIR = agentosHome("prompts");
|
|
26
|
+
export const BUS_BRIEF_FILE = agentosHome("bus-brief.md");
|
|
31
27
|
|
|
32
28
|
export function ensureBusBrief(): void {
|
|
33
29
|
fs.mkdirSync(path.dirname(BUS_BRIEF_FILE), { recursive: true });
|
package/lib/artifacts/store.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* ~/.agent-os/artifacts/<session id>/, and a row saying whose it is.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
import { agentosHome } from "../agentos-home";
|
|
6
7
|
import fs from "fs";
|
|
7
|
-
import os from "os";
|
|
8
8
|
import path from "path";
|
|
9
9
|
import { randomUUID } from "crypto";
|
|
10
10
|
import { db } from "../db";
|
|
@@ -23,10 +23,7 @@ export const MAX_TITLE_CHARS = 120;
|
|
|
23
23
|
const ID = /^[0-9a-f-]{36}$/;
|
|
24
24
|
|
|
25
25
|
export function artifactsDir(): string {
|
|
26
|
-
return (
|
|
27
|
-
process.env.AGENTOS_ARTIFACTS_DIR ||
|
|
28
|
-
path.join(os.homedir(), ".agent-os", "artifacts")
|
|
29
|
-
);
|
|
26
|
+
return process.env.AGENTOS_ARTIFACTS_DIR || agentosHome("artifacts");
|
|
30
27
|
}
|
|
31
28
|
|
|
32
29
|
// Session ids are UUIDs; anything else must not become a path segment.
|
package/lib/chat/driver.ts
CHANGED
|
@@ -11,9 +11,26 @@ import type {
|
|
|
11
11
|
ChatModel,
|
|
12
12
|
DriverEvent,
|
|
13
13
|
FileSuggestion,
|
|
14
|
+
McpServerView,
|
|
15
|
+
McpSignInStart,
|
|
14
16
|
UndoPreview,
|
|
15
17
|
} from "./events";
|
|
16
18
|
|
|
19
|
+
// An agent's way of signing in to an MCP server that needs it, without
|
|
20
|
+
// restarting the conversation. Tokens stay with the agent; the host only
|
|
21
|
+
// sees the link to open and, later, the address the provider returned to.
|
|
22
|
+
export interface McpAuth {
|
|
23
|
+
// `state` ties the provider's return to this attempt; it stays server-side.
|
|
24
|
+
start(
|
|
25
|
+
server: string,
|
|
26
|
+
redirectUri?: string
|
|
27
|
+
): Promise<McpSignInStart & { state?: string }>;
|
|
28
|
+
// Hands over the address the provider returned the browser to.
|
|
29
|
+
submit(server: string, callbackUrl: string): Promise<void>;
|
|
30
|
+
reconnect(server: string): Promise<void>;
|
|
31
|
+
servers(): Promise<McpServerView[]>;
|
|
32
|
+
}
|
|
33
|
+
|
|
17
34
|
export interface ChatStartOptions {
|
|
18
35
|
cwd: string;
|
|
19
36
|
model: string;
|
|
@@ -39,7 +56,8 @@ export interface ChatStartOptions {
|
|
|
39
56
|
// can't be changed for this conversation.
|
|
40
57
|
permissionMode?: PermissionMode;
|
|
41
58
|
// Checked before every tool call, whatever the permissions say: why it's
|
|
42
|
-
// refused,
|
|
59
|
+
// refused, null to let it go on to them, or a warning the agent is shown
|
|
60
|
+
// while the call still goes on.
|
|
43
61
|
toolGuard?: ToolGuard;
|
|
44
62
|
// The note handed back to the agent right after its context is compacted
|
|
45
63
|
// (lib/chat/compact-context), empty for none. Only Claude Code has a hook
|
|
@@ -47,10 +65,12 @@ export interface ChatStartOptions {
|
|
|
47
65
|
compactContext?: () => Promise<string>;
|
|
48
66
|
}
|
|
49
67
|
|
|
68
|
+
export type ToolGuardResult = string | null | { warning: string };
|
|
69
|
+
|
|
50
70
|
export type ToolGuard = (
|
|
51
71
|
tool: string,
|
|
52
72
|
input: Record<string, unknown>
|
|
53
|
-
) => Promise<
|
|
73
|
+
) => Promise<ToolGuardResult>;
|
|
54
74
|
|
|
55
75
|
// One live conversation with an agent. Messages sent while a turn runs are
|
|
56
76
|
// queued by the provider and folded into the conversation.
|
|
@@ -88,6 +108,8 @@ export interface ChatConversation {
|
|
|
88
108
|
backgroundTask?(toolUseId?: string): Promise<boolean>;
|
|
89
109
|
// Puts files back as they were before a message (dryRun: only say what).
|
|
90
110
|
undo(checkpoint: string, dryRun: boolean): Promise<UndoResult>;
|
|
111
|
+
// Signing in to an MCP server that needs it (Claude's).
|
|
112
|
+
mcpAuth?: McpAuth;
|
|
91
113
|
close(): void;
|
|
92
114
|
events: AsyncIterable<DriverEvent>;
|
|
93
115
|
}
|
|
@@ -248,6 +248,13 @@ export function guardHook(guard: ToolGuard): HookCallback {
|
|
|
248
248
|
(input.tool_input ?? {}) as Record<string, unknown>
|
|
249
249
|
).catch((e: Error) => `Couldn't check this call: ${e.message}`);
|
|
250
250
|
if (reason === null) return {};
|
|
251
|
+
if (typeof reason !== "string")
|
|
252
|
+
return {
|
|
253
|
+
hookSpecificOutput: {
|
|
254
|
+
hookEventName: "PreToolUse",
|
|
255
|
+
additionalContext: reason.warning,
|
|
256
|
+
},
|
|
257
|
+
};
|
|
251
258
|
return {
|
|
252
259
|
hookSpecificOutput: {
|
|
253
260
|
hookEventName: "PreToolUse",
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import type { McpServerStatus, Query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import type { McpAuth } from "../driver";
|
|
3
|
+
import type { McpServerView, McpSignInStart } from "../events";
|
|
4
|
+
|
|
5
|
+
// The CLI takes these as control requests and the SDK's Query has the
|
|
6
|
+
// methods, but its type declarations don't list them.
|
|
7
|
+
type AuthQuery = Query & {
|
|
8
|
+
mcpAuthenticate?(server: string, redirectUri?: string): Promise<unknown>;
|
|
9
|
+
mcpSubmitOAuthCallbackUrl?(server: string, url: string): Promise<unknown>;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const LOOPBACK = new Set(["localhost", "127.0.0.1", "[::1]"]);
|
|
13
|
+
|
|
14
|
+
// Only a link a person can safely open: https, or http to this machine.
|
|
15
|
+
export function safeAuthUrl(value: unknown): string | null {
|
|
16
|
+
if (typeof value !== "string") return null;
|
|
17
|
+
try {
|
|
18
|
+
const url = new URL(value);
|
|
19
|
+
if (url.protocol === "https:") return url.href;
|
|
20
|
+
if (url.protocol === "http:" && LOOPBACK.has(url.hostname)) return url.href;
|
|
21
|
+
} catch {
|
|
22
|
+
// Not a URL.
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The CLI's answer to mcp_authenticate, as what the UI needs.
|
|
28
|
+
export function toSignInStart(
|
|
29
|
+
response: unknown
|
|
30
|
+
): McpSignInStart & { state?: string } {
|
|
31
|
+
const r = (response ?? {}) as {
|
|
32
|
+
authUrl?: unknown;
|
|
33
|
+
requiresUserAction?: unknown;
|
|
34
|
+
callbackExpected?: unknown;
|
|
35
|
+
redirectScheme?: unknown;
|
|
36
|
+
state?: unknown;
|
|
37
|
+
};
|
|
38
|
+
if (!r.requiresUserAction && r.authUrl === undefined) return { mode: "done" };
|
|
39
|
+
const authUrl = safeAuthUrl(r.authUrl);
|
|
40
|
+
if (!authUrl)
|
|
41
|
+
throw new Error("The server gave a sign-in link AgentOS won't open");
|
|
42
|
+
if (!r.callbackExpected) return { mode: "external", authUrl };
|
|
43
|
+
const state = typeof r.state === "string" ? r.state : undefined;
|
|
44
|
+
return r.redirectScheme === "custom"
|
|
45
|
+
? { mode: "callback", authUrl, state }
|
|
46
|
+
: { mode: "paste", authUrl, state };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Whether a server can be signed in to: it speaks OAuth over HTTP.
|
|
50
|
+
export function canSignIn(s: McpServerStatus): boolean {
|
|
51
|
+
if (s.status !== "needs-auth") return false;
|
|
52
|
+
const c = s.config as { type?: string; command?: unknown } | undefined;
|
|
53
|
+
if (!c) return true;
|
|
54
|
+
return c.type === "http" || c.type === "sse" || c.type === "claudeai-proxy";
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function claudeMcpAuth(
|
|
58
|
+
q: Query,
|
|
59
|
+
view: (s: McpServerStatus) => McpServerView
|
|
60
|
+
): McpAuth {
|
|
61
|
+
const auth = q as AuthQuery;
|
|
62
|
+
return {
|
|
63
|
+
async start(server, redirectUri) {
|
|
64
|
+
if (!auth.mcpAuthenticate)
|
|
65
|
+
throw new Error(
|
|
66
|
+
"This Claude Code version can't sign in to MCP servers from AgentOS"
|
|
67
|
+
);
|
|
68
|
+
return toSignInStart(await auth.mcpAuthenticate(server, redirectUri));
|
|
69
|
+
},
|
|
70
|
+
async submit(server, callbackUrl) {
|
|
71
|
+
if (!auth.mcpSubmitOAuthCallbackUrl)
|
|
72
|
+
throw new Error(
|
|
73
|
+
"This Claude Code version can't sign in to MCP servers from AgentOS"
|
|
74
|
+
);
|
|
75
|
+
await auth.mcpSubmitOAuthCallbackUrl(server, callbackUrl);
|
|
76
|
+
},
|
|
77
|
+
reconnect: (server) => q.reconnectMcpServer(server),
|
|
78
|
+
async servers() {
|
|
79
|
+
return (await q.mcpServerStatus()).map(view);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
@@ -29,6 +29,7 @@ import { NO_ATTRIBUTION } from "../../agents/attribution";
|
|
|
29
29
|
import { SUBAGENTS_ENV } from "../../models/subagent-env";
|
|
30
30
|
import { parseSubagentEnv, subagentModelHook } from "../../models/subagents";
|
|
31
31
|
import { callFromAssistant } from "../../usage/calls";
|
|
32
|
+
import { canSignIn, claudeMcpAuth } from "./claude-mcp-auth";
|
|
32
33
|
|
|
33
34
|
// Claude Code through the Agent SDK, signed in with the user's own Claude
|
|
34
35
|
// Code login. Asks for approval through chat cards unless given full access,
|
|
@@ -71,6 +72,10 @@ function cleanError(text?: string): string | undefined {
|
|
|
71
72
|
return line.length > 300 ? `${line.slice(0, 299)}…` : line;
|
|
72
73
|
}
|
|
73
74
|
|
|
75
|
+
export function cleanMcpError(text?: string): string | undefined {
|
|
76
|
+
return cleanError(text);
|
|
77
|
+
}
|
|
78
|
+
|
|
74
79
|
export function mcpView(s: McpServerStatus): McpServerView {
|
|
75
80
|
return {
|
|
76
81
|
name: s.name,
|
|
@@ -78,6 +83,7 @@ export function mcpView(s: McpServerStatus): McpServerView {
|
|
|
78
83
|
scope: s.source ?? s.scope,
|
|
79
84
|
version: s.serverInfo?.version,
|
|
80
85
|
error: cleanError(s.error),
|
|
86
|
+
...(canSignIn(s) && { signIn: true }),
|
|
81
87
|
tools: (s.tools ?? []).map((t) => ({
|
|
82
88
|
name: t.name,
|
|
83
89
|
description: firstLine(t.description),
|
|
@@ -346,6 +352,7 @@ export const claudeDriver: ChatDriver = {
|
|
|
346
352
|
},
|
|
347
353
|
]);
|
|
348
354
|
},
|
|
355
|
+
mcpAuth: claudeMcpAuth(q, mcpView),
|
|
349
356
|
async interrupt() {
|
|
350
357
|
await q.interrupt();
|
|
351
358
|
},
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { agentosHome } from "../../agentos-home";
|
|
1
2
|
import fs from "fs";
|
|
2
|
-
import os from "os";
|
|
3
3
|
import path from "path";
|
|
4
4
|
import { randomUUID } from "crypto";
|
|
5
5
|
import type { ChatAccess, DriverEvent } from "../events";
|
|
@@ -9,7 +9,7 @@ import { piTool } from "./pi-mapper";
|
|
|
9
9
|
type P = Record<string, unknown>;
|
|
10
10
|
const str = (v: unknown) => (typeof v === "string" ? v : "");
|
|
11
11
|
|
|
12
|
-
const DIR =
|
|
12
|
+
const DIR = agentosHome("pi");
|
|
13
13
|
const MARK = "agentos:";
|
|
14
14
|
|
|
15
15
|
// Pi runs every tool without asking. This extension asks the chat first,
|
package/lib/chat/events.ts
CHANGED
|
@@ -104,9 +104,21 @@ export interface McpServerView {
|
|
|
104
104
|
scope?: string;
|
|
105
105
|
version?: string;
|
|
106
106
|
error?: string;
|
|
107
|
+
// Needs sign-in and AgentOS can start it (the driver can, and the server
|
|
108
|
+
// speaks OAuth over HTTP). Decided by the server, never by its name.
|
|
109
|
+
signIn?: boolean;
|
|
107
110
|
tools: { name: string; description?: string }[];
|
|
108
111
|
}
|
|
109
112
|
|
|
113
|
+
// What starting an MCP server's sign-in came back with: "callback" (open the
|
|
114
|
+
// link; the provider returns to AgentOS), "paste" (the provider only returns
|
|
115
|
+
// to the machine running the session: paste the address it ends on),
|
|
116
|
+
// "external" (finish on claude.ai) or "done" (signed in without asking).
|
|
117
|
+
export interface McpSignInStart {
|
|
118
|
+
mode: "callback" | "paste" | "external" | "done";
|
|
119
|
+
authUrl?: string;
|
|
120
|
+
}
|
|
121
|
+
|
|
110
122
|
// How full the conversation's context window is, as the agent last measured.
|
|
111
123
|
export interface ChatContext {
|
|
112
124
|
usedTokens: number;
|