@saadnvd1/agent-os 0.3.817 → 0.3.821
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/client-config/route.test.ts +3 -1
- package/app/api/orchestrators/[workspaceId]/intake/route.test.ts +77 -0
- package/app/api/owner/route.test.ts +27 -0
- package/app/api/owner/route.ts +17 -0
- package/app/api/workspaces/[id]/orchestrator/rules/[ruleId]/route.ts +2 -2
- package/app/api/workspaces/[id]/orchestrator/rules/presence.ts +1 -1
- package/app/api/workspaces/[id]/orchestrator/rules/route.ts +2 -2
- package/app/api/workspaces/[id]/orchestrator/tools/route.ts +6 -1
- package/app/api/worktree-sweep/route.ts +2 -2
- package/components/Chat/EventRow.tsx +5 -2
- package/components/Chat/Messages.tsx +9 -2
- package/components/SessionList/statusDot.test.ts +1 -1
- package/components/Settings/OwnerNameRow.tsx +55 -0
- package/components/Settings/SetupSection.tsx +2 -0
- package/content/whats-new.json +16 -0
- package/data/client-config/queries.ts +3 -0
- package/data/owner/mutations.ts +19 -0
- package/lib/approvals/enable.ts +2 -1
- package/lib/artifacts/tools.ts +2 -1
- package/lib/away/brief.ts +2 -1
- package/lib/away/run.ts +5 -4
- package/lib/away/sweep.ts +2 -1
- package/lib/chat/runner.ts +2 -1
- package/lib/chat/turns.ts +1 -1
- package/lib/config/client-config-types.ts +5 -1
- package/lib/config/client-config.ts +2 -0
- package/lib/intake/accept.ts +58 -0
- package/lib/intake/intake.ts +44 -17
- package/lib/intake/message.ts +6 -2
- package/lib/learning-cards/analyze.ts +2 -1
- package/lib/models/plan-step.ts +2 -1
- package/lib/orchestrator/approval-carry.ts +2 -1
- package/lib/orchestrator/armed.ts +9 -8
- package/lib/orchestrator/ask-heads.ts +2 -1
- package/lib/orchestrator/ask-tool.ts +6 -5
- package/lib/orchestrator/asks.ts +6 -4
- package/lib/orchestrator/brakes.ts +3 -2
- package/lib/orchestrator/brief.ts +61 -50
- package/lib/orchestrator/cross.ts +2 -1
- package/lib/orchestrator/describe.ts +5 -2
- package/lib/orchestrator/digest.ts +4 -3
- package/lib/orchestrator/escalate.ts +7 -6
- package/lib/orchestrator/external-gates.ts +5 -4
- package/lib/orchestrator/held-asks.ts +2 -2
- package/lib/orchestrator/held.ts +3 -2
- package/lib/orchestrator/mcp-servers.ts +1 -1
- package/lib/orchestrator/pause.ts +4 -3
- package/lib/orchestrator/questions.ts +2 -1
- package/lib/orchestrator/recipe-tools.ts +5 -4
- package/lib/orchestrator/review.ts +4 -3
- package/lib/orchestrator/rules.ts +2 -1
- package/lib/orchestrator/serve.ts +1 -1
- package/lib/orchestrator/shell-guard.ts +4 -4
- package/lib/orchestrator/signoff.test-helpers.ts +1 -1
- package/lib/orchestrator/signoff.ts +10 -7
- package/lib/orchestrator/sweep-arm.ts +2 -1
- package/lib/orchestrator/tool-names.ts +1 -1
- package/lib/orchestrator/tool-schemas.ts +13 -13
- package/lib/orchestrator/tools.ts +21 -21
- package/lib/owner-label.ts +25 -0
- package/lib/owner.ts +21 -0
- package/lib/project-add/index.ts +2 -1
- package/lib/schedules/index.ts +2 -1
- package/lib/session-logs/gather.ts +3 -2
- package/lib/tasks/queue.ts +3 -1
- package/lib/tasks/start.ts +3 -1
- package/package.json +1 -1
|
@@ -6,6 +6,8 @@ describe("GET /api/client-config", () => {
|
|
|
6
6
|
const res = GET();
|
|
7
7
|
expect(res.status).toBe(200);
|
|
8
8
|
expect(res.headers.get("cache-control")).toBe("no-store");
|
|
9
|
-
|
|
9
|
+
const body = await res.json();
|
|
10
|
+
expect(body.v).toBe(1);
|
|
11
|
+
expect(body).toHaveProperty("ownerName");
|
|
10
12
|
});
|
|
11
13
|
});
|
|
@@ -193,6 +193,83 @@ describe("POST /api/orchestrators/:id/intake", () => {
|
|
|
193
193
|
expect(sent).toHaveLength(0);
|
|
194
194
|
});
|
|
195
195
|
|
|
196
|
+
it("takes a HAR over the old 25 MB cap, redacted, and names it by path", async () => {
|
|
197
|
+
const { ws, device } = setup();
|
|
198
|
+
const secret = ["tok", crypto.randomUUID()].join("-");
|
|
199
|
+
const har = JSON.stringify({
|
|
200
|
+
log: {
|
|
201
|
+
version: "1.2",
|
|
202
|
+
entries: [
|
|
203
|
+
{
|
|
204
|
+
request: {
|
|
205
|
+
method: "GET",
|
|
206
|
+
url: "https://api.example.com/v1/me",
|
|
207
|
+
headers: [{ name: "Authorization", value: `Bearer ${secret}` }],
|
|
208
|
+
},
|
|
209
|
+
response: {
|
|
210
|
+
status: 200,
|
|
211
|
+
headers: [],
|
|
212
|
+
content: { size: 30_000_000, text: "x".repeat(30_000_000) },
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
expect(Buffer.byteLength(har)).toBeGreaterThan(25_000_000);
|
|
219
|
+
const res = await post(ws, device, { clientId: cid(), kind: "share" }, [
|
|
220
|
+
{ name: "capture.har", data: Buffer.from(har) },
|
|
221
|
+
]);
|
|
222
|
+
expect(res.status).toBe(201);
|
|
223
|
+
const body = await res.json();
|
|
224
|
+
const saved = fs.readFileSync(path.join(body.dir, "capture.har"), "utf8");
|
|
225
|
+
expect(saved).not.toContain(secret);
|
|
226
|
+
expect(saved).toContain("[redacted]");
|
|
227
|
+
expect(
|
|
228
|
+
fs.readFileSync(path.join(body.dir, "capture.har.summary.txt"), "utf8")
|
|
229
|
+
).toContain("1 requests");
|
|
230
|
+
const text = (sent[0].input as { text: string }).text;
|
|
231
|
+
expect(text).toContain(path.join(body.dir, "capture.har"));
|
|
232
|
+
expect(text).toContain("large: read it in parts");
|
|
233
|
+
expect(text).not.toContain("xxxxxxxxxx");
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("takes a shell script as text, with its #! line", async () => {
|
|
237
|
+
const { ws, device } = setup();
|
|
238
|
+
const res = await post(ws, device, { clientId: cid(), kind: "share" }, [
|
|
239
|
+
{ name: "deploy.sh", data: Buffer.from("#!/bin/sh\necho hi\n") },
|
|
240
|
+
]);
|
|
241
|
+
expect(res.status).toBe(201);
|
|
242
|
+
const { dir } = await res.json();
|
|
243
|
+
expect(fs.readFileSync(path.join(dir, "deploy.sh"), "utf8")).toContain(
|
|
244
|
+
"#!/bin/sh"
|
|
245
|
+
);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
it("still refuses a Mach-O or ELF binary, even named like a script", async () => {
|
|
249
|
+
const { ws, device } = setup();
|
|
250
|
+
const macho = Buffer.from([0xcf, 0xfa, 0xed, 0xfe, 7, 0, 0, 1, 0, 0]);
|
|
251
|
+
const elf = Buffer.from([0x7f, 0x45, 0x4c, 0x46, 2, 1, 1, 0, 0, 0]);
|
|
252
|
+
for (const name of ["tool", "tool.sh", "tool.exe"])
|
|
253
|
+
for (const data of [macho, elf]) {
|
|
254
|
+
const res = await post(ws, device, { clientId: cid(), kind: "share" }, [
|
|
255
|
+
{ name, data },
|
|
256
|
+
]);
|
|
257
|
+
expect([400, 415]).toContain(res.status);
|
|
258
|
+
}
|
|
259
|
+
expect(sent).toHaveLength(0);
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
it("holds an image to the image limit", async () => {
|
|
263
|
+
const { ws, device } = setup();
|
|
264
|
+
const res = await post(ws, device, { clientId: cid(), kind: "share" }, [
|
|
265
|
+
{
|
|
266
|
+
name: "big.png",
|
|
267
|
+
data: Buffer.concat([png(), Buffer.alloc(4_000_000)]),
|
|
268
|
+
},
|
|
269
|
+
]);
|
|
270
|
+
expect(res.status).toBe(413);
|
|
271
|
+
});
|
|
272
|
+
|
|
196
273
|
it("rejects bad meta", async () => {
|
|
197
274
|
const { ws, device } = setup();
|
|
198
275
|
expect((await post(ws, device, "{nope")).status).toBe(400);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { GET, PUT } from "./route";
|
|
3
|
+
|
|
4
|
+
const put = (body: unknown) =>
|
|
5
|
+
PUT(
|
|
6
|
+
new Request("http://localhost/api/owner", {
|
|
7
|
+
method: "PUT",
|
|
8
|
+
body: JSON.stringify(body),
|
|
9
|
+
})
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
describe("/api/owner", () => {
|
|
13
|
+
it("sets, reads and clears the owner's name", async () => {
|
|
14
|
+
expect(await (await GET()).json()).toEqual({ name: null });
|
|
15
|
+
expect(await (await put({ name: " Alex " })).json()).toEqual({
|
|
16
|
+
name: "Alex",
|
|
17
|
+
});
|
|
18
|
+
expect(await (await GET()).json()).toEqual({ name: "Alex" });
|
|
19
|
+
expect(await (await put({ name: null })).json()).toEqual({ name: null });
|
|
20
|
+
expect(await (await GET()).json()).toEqual({ name: null });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("refuses a name that isn't text", async () => {
|
|
24
|
+
expect((await put({ name: 5 })).status).toBe(400);
|
|
25
|
+
expect((await put({})).status).toBe(400);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { ownerName, setOwnerName } from "@/lib/owner";
|
|
3
|
+
|
|
4
|
+
// GET /api/owner - the name the owner gave, or null.
|
|
5
|
+
export function GET() {
|
|
6
|
+
return NextResponse.json({ name: ownerName() });
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// PUT /api/owner { name } - set it; blank or null clears it.
|
|
10
|
+
export async function PUT(request: Request) {
|
|
11
|
+
const body = (await request.json().catch(() => null)) as {
|
|
12
|
+
name?: unknown;
|
|
13
|
+
} | null;
|
|
14
|
+
if (!body || (body.name !== null && typeof body.name !== "string"))
|
|
15
|
+
return NextResponse.json({ error: "name must be text" }, { status: 400 });
|
|
16
|
+
return NextResponse.json({ name: setOwnerName(body.name) });
|
|
17
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import type { PresenceProof } from "@/lib/security/presence-http";
|
|
3
3
|
import { removeRule, updateRule } from "@/lib/orchestrator/rules";
|
|
4
|
-
import {
|
|
4
|
+
import { provenAsOwner } from "../presence";
|
|
5
5
|
|
|
6
6
|
type RouteParams = { params: Promise<{ id: string; ruleId: string }> };
|
|
7
7
|
|
|
@@ -21,7 +21,7 @@ async function target(
|
|
|
21
21
|
response: NextResponse.json({ error: "Bad rule id" }, { status: 400 }),
|
|
22
22
|
};
|
|
23
23
|
const body = (await request.json().catch(() => ({}))) as Body;
|
|
24
|
-
const refused = await
|
|
24
|
+
const refused = await provenAsOwner(request, id, body);
|
|
25
25
|
if (refused) return { response: refused };
|
|
26
26
|
return { workspaceId: id, ruleId: n, body };
|
|
27
27
|
}
|
|
@@ -10,7 +10,7 @@ import { rulesPresence } from "@/lib/orchestrator/presence-binding";
|
|
|
10
10
|
// A rule written here is Saad's, shown to the orchestrator as his word and
|
|
11
11
|
// removable only by him: so it takes his passkey, not just a trusted place
|
|
12
12
|
// (any process on this machine reaches the route).
|
|
13
|
-
export async function
|
|
13
|
+
export async function provenAsOwner(
|
|
14
14
|
request: NextRequest,
|
|
15
15
|
workspaceId: string,
|
|
16
16
|
proof: PresenceProof | undefined
|
|
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
|
|
|
2
2
|
import type { PresenceProof } from "@/lib/security/presence-http";
|
|
3
3
|
import { getWorkspace } from "@/lib/workspaces";
|
|
4
4
|
import { addRule, listRules } from "@/lib/orchestrator/rules";
|
|
5
|
-
import {
|
|
5
|
+
import { provenAsOwner } from "./presence";
|
|
6
6
|
|
|
7
7
|
type RouteParams = { params: Promise<{ id: string }> };
|
|
8
8
|
|
|
@@ -25,7 +25,7 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
|
|
25
25
|
const { text } = body;
|
|
26
26
|
if (typeof text !== "string")
|
|
27
27
|
return NextResponse.json({ error: "text is required" }, { status: 400 });
|
|
28
|
-
const refused = await
|
|
28
|
+
const refused = await provenAsOwner(request, id, body);
|
|
29
29
|
if (refused) return refused;
|
|
30
30
|
try {
|
|
31
31
|
return NextResponse.json({ rule: addRule(id, text, "saad") });
|
|
@@ -5,6 +5,8 @@ import { TOKEN_HEADER } from "@/lib/orchestrator/tools";
|
|
|
5
5
|
|
|
6
6
|
type RouteParams = { params: Promise<{ id: string }> };
|
|
7
7
|
|
|
8
|
+
const LEGACY_ASK = "ask_saad";
|
|
9
|
+
|
|
8
10
|
// The orchestrator chat's tools call here from its worker: plain text back.
|
|
9
11
|
// Only that workspace's orchestrator worker holds the secret: other local
|
|
10
12
|
// sessions are trusted by the network, not to act as the orchestrator.
|
|
@@ -12,7 +14,10 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
|
|
12
14
|
const { id } = await params;
|
|
13
15
|
if (!isOrchestratorToken(id, request.headers.get(TOKEN_HEADER)))
|
|
14
16
|
return new NextResponse("Not the orchestrator", { status: 403 });
|
|
15
|
-
const
|
|
17
|
+
const body = await request.json().catch(() => ({}));
|
|
18
|
+
const { args } = body;
|
|
19
|
+
// A worker started before the rename still calls ask_saad.
|
|
20
|
+
const tool = body.tool === LEGACY_ASK ? "ask_owner" : body.tool;
|
|
16
21
|
if (!isToolName(tool))
|
|
17
22
|
return NextResponse.json({ error: "Unknown tool" }, { status: 400 });
|
|
18
23
|
try {
|
|
@@ -10,6 +10,7 @@ import { requireLocalTrust } from "@/lib/security/route-guard";
|
|
|
10
10
|
import { db, type Session } from "@/lib/db";
|
|
11
11
|
import { orchestratorFullAccess } from "@/lib/orchestrator/full-access";
|
|
12
12
|
import { sweepState } from "@/lib/worktree-sweep/state";
|
|
13
|
+
import { owner } from "@/lib/owner";
|
|
13
14
|
|
|
14
15
|
const body = z.union([
|
|
15
16
|
z.object({
|
|
@@ -53,8 +54,7 @@ export async function PUT(request: NextRequest) {
|
|
|
53
54
|
if (!s || !orchestratorFullAccess(s))
|
|
54
55
|
return NextResponse.json(
|
|
55
56
|
{
|
|
56
|
-
error:
|
|
57
|
-
"a session can't turn the full clean-up on: Saad does (Health, Clean-up, Turn on), or the orchestrator's sweep_arm tool when he has said to",
|
|
57
|
+
error: `a session can't turn the full clean-up on: ${owner()} does (Health, Clean-up, Turn on), or the orchestrator's sweep_arm tool when he has said to`,
|
|
58
58
|
},
|
|
59
59
|
{ status: 403 }
|
|
60
60
|
);
|
|
@@ -18,6 +18,8 @@ import { cn } from "@/lib/utils";
|
|
|
18
18
|
import { UserMessage } from "./Messages";
|
|
19
19
|
import { openSessionLink as openSession } from "./session-link";
|
|
20
20
|
import { useRowState } from "./rowState";
|
|
21
|
+
import { useOwnerName } from "@/data/client-config/queries";
|
|
22
|
+
import { ownerInUi } from "@/lib/owner-label";
|
|
21
23
|
|
|
22
24
|
type UserItem = Extract<ChatItem, { kind: "user" }>;
|
|
23
25
|
|
|
@@ -158,6 +160,7 @@ export function EventRow({
|
|
|
158
160
|
origin: ChatOrigin;
|
|
159
161
|
}) {
|
|
160
162
|
const { body, decided } = eventParts(item, origin);
|
|
163
|
+
const decidedLabel = `${ownerInUi(useOwnerName(), true)} decided`;
|
|
161
164
|
const decision = origin.kind === "decision";
|
|
162
165
|
return (
|
|
163
166
|
<div className="flex flex-col gap-1">
|
|
@@ -167,7 +170,7 @@ export function EventRow({
|
|
|
167
170
|
id={`${item.id}:${i}`}
|
|
168
171
|
kind="decision"
|
|
169
172
|
tone="decision"
|
|
170
|
-
head={<span className="truncate font-medium">
|
|
173
|
+
head={<span className="truncate font-medium">{decidedLabel}</span>}
|
|
171
174
|
body={d}
|
|
172
175
|
at={item.createdAt}
|
|
173
176
|
/>
|
|
@@ -179,7 +182,7 @@ export function EventRow({
|
|
|
179
182
|
tone={decision ? "decision" : undefined}
|
|
180
183
|
head={
|
|
181
184
|
decision ? (
|
|
182
|
-
<span className="truncate font-medium">
|
|
185
|
+
<span className="truncate font-medium">{decidedLabel}</span>
|
|
183
186
|
) : origin.kind === "guest" ? (
|
|
184
187
|
<span className="truncate font-medium">
|
|
185
188
|
Suggested by {origin.label}
|
|
@@ -39,6 +39,8 @@ import {
|
|
|
39
39
|
TurnBookmark,
|
|
40
40
|
BookmarkPress,
|
|
41
41
|
} from "@/components/Bookmarks/BookmarkButton";
|
|
42
|
+
import { useOwnerName } from "@/data/client-config/queries";
|
|
43
|
+
import { ownerVerb } from "@/lib/owner-label";
|
|
42
44
|
|
|
43
45
|
type Of<K extends ChatItem["kind"]> = Extract<ChatItem, { kind: K }>;
|
|
44
46
|
|
|
@@ -473,13 +475,18 @@ export function CommandOutput({ item }: { item: Of<"command_output"> }) {
|
|
|
473
475
|
const NOTE_LABEL = {
|
|
474
476
|
note: "Note",
|
|
475
477
|
brake: "Brake",
|
|
476
|
-
escalation: "
|
|
478
|
+
escalation: "",
|
|
477
479
|
ask: "Ask",
|
|
478
480
|
pause: "Pause",
|
|
479
481
|
away: "Away",
|
|
480
482
|
} as const;
|
|
481
483
|
|
|
482
484
|
export function NoteLine({ item }: { item: Of<"note"> }) {
|
|
485
|
+
const name = useOwnerName();
|
|
486
|
+
const label =
|
|
487
|
+
item.tone === "escalation"
|
|
488
|
+
? ownerVerb(name, "decides", "decide")
|
|
489
|
+
: NOTE_LABEL[item.tone];
|
|
483
490
|
return (
|
|
484
491
|
<div
|
|
485
492
|
className={cn(
|
|
@@ -490,7 +497,7 @@ export function NoteLine({ item }: { item: Of<"note"> }) {
|
|
|
490
497
|
)}
|
|
491
498
|
>
|
|
492
499
|
<span className="text-muted-foreground mr-2 font-mono text-[11px] tracking-wide uppercase">
|
|
493
|
-
{
|
|
500
|
+
{label}
|
|
494
501
|
</span>
|
|
495
502
|
{item.text}
|
|
496
503
|
</div>
|
|
@@ -35,7 +35,7 @@ describe("statusDot", () => {
|
|
|
35
35
|
expect(d.title).toContain("Load 4.2 on 10 cores");
|
|
36
36
|
expect(d.opensHealth).toBe(true);
|
|
37
37
|
});
|
|
38
|
-
it("goes red when a health issue needs
|
|
38
|
+
it("goes red when a health issue needs the owner", () => {
|
|
39
39
|
const d = statusDot(load("green"), { open: 2, needsYou: 2 })!;
|
|
40
40
|
expect(d.level).toBe("red");
|
|
41
41
|
expect(d.label).toBe("Load normal · 2 health issues need you");
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Input } from "@/components/ui/input";
|
|
6
|
+
import { useClientConfig } from "@/data/client-config/queries";
|
|
7
|
+
import { useSetOwnerName } from "@/data/owner/mutations";
|
|
8
|
+
|
|
9
|
+
// The name AgentOS and the orchestrators use when they refer to you.
|
|
10
|
+
export function OwnerNameRow() {
|
|
11
|
+
const saved = useClientConfig().data?.ownerName ?? "";
|
|
12
|
+
// null: show what is saved.
|
|
13
|
+
const [draft, setDraft] = useState<string | null>(null);
|
|
14
|
+
const value = draft ?? saved;
|
|
15
|
+
const save = useSetOwnerName();
|
|
16
|
+
return (
|
|
17
|
+
<form
|
|
18
|
+
className="bg-muted/40 flex flex-col gap-2 rounded-lg px-3 py-2.5 sm:flex-row sm:items-center"
|
|
19
|
+
onSubmit={(e) => {
|
|
20
|
+
e.preventDefault();
|
|
21
|
+
save.mutate(value.trim() || null, { onSuccess: () => setDraft(null) });
|
|
22
|
+
}}
|
|
23
|
+
>
|
|
24
|
+
<div className="min-w-0 flex-1">
|
|
25
|
+
<label htmlFor="owner-name" className="text-sm font-medium">
|
|
26
|
+
Your name
|
|
27
|
+
</label>
|
|
28
|
+
<p className="text-muted-foreground text-xs">
|
|
29
|
+
Used where AgentOS and your orchestrators refer to you. Orchestrators
|
|
30
|
+
pick it up when they next start.
|
|
31
|
+
</p>
|
|
32
|
+
{save.error && (
|
|
33
|
+
<p className="text-destructive text-xs">{save.error.message}</p>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
<Input
|
|
37
|
+
id="owner-name"
|
|
38
|
+
value={value}
|
|
39
|
+
maxLength={40}
|
|
40
|
+
placeholder="Not set"
|
|
41
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
42
|
+
className="min-h-11 sm:min-h-8 sm:w-44"
|
|
43
|
+
/>
|
|
44
|
+
<Button
|
|
45
|
+
type="submit"
|
|
46
|
+
size="sm"
|
|
47
|
+
variant="outline"
|
|
48
|
+
className="min-h-11 sm:min-h-8"
|
|
49
|
+
disabled={save.isPending || value.trim() === saved}
|
|
50
|
+
>
|
|
51
|
+
Save
|
|
52
|
+
</Button>
|
|
53
|
+
</form>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -4,6 +4,7 @@ import { Button } from "@/components/ui/button";
|
|
|
4
4
|
import { HealthList } from "@/components/Welcome/HealthList";
|
|
5
5
|
import { AgentList } from "@/components/Welcome/AgentList";
|
|
6
6
|
import { ReachRow } from "@/components/Reach/ReachRow";
|
|
7
|
+
import { OwnerNameRow } from "./OwnerNameRow";
|
|
7
8
|
import { settingsUiActions } from "@/stores/settingsUi";
|
|
8
9
|
import { welcomeUiActions } from "@/stores/welcomeUi";
|
|
9
10
|
|
|
@@ -11,6 +12,7 @@ import { welcomeUiActions } from "@/stores/welcomeUi";
|
|
|
11
12
|
export function SetupSection() {
|
|
12
13
|
return (
|
|
13
14
|
<div className="flex flex-col gap-4">
|
|
15
|
+
<OwnerNameRow />
|
|
14
16
|
<div className="bg-muted/40 flex items-center gap-3 rounded-lg px-3 py-2.5">
|
|
15
17
|
<div className="min-w-0 flex-1">
|
|
16
18
|
<p className="text-sm font-medium">First-run setup</p>
|
package/content/whats-new.json
CHANGED
|
@@ -1905,6 +1905,22 @@
|
|
|
1905
1905
|
"area": "Chat",
|
|
1906
1906
|
"text": "Chats now accept HAR network captures (with logins and cookies hidden), iPhone photos and many more file types.",
|
|
1907
1907
|
"url": "https://github.com/saadnvd1/agent-os/pull/548"
|
|
1908
|
+
},
|
|
1909
|
+
{
|
|
1910
|
+
"id": 549,
|
|
1911
|
+
"date": "2026-11-01",
|
|
1912
|
+
"kind": "improved",
|
|
1913
|
+
"area": "AgentOS",
|
|
1914
|
+
"text": "Files shared into the app are now checked against the same rules as chat attachments.",
|
|
1915
|
+
"url": "https://github.com/saadnvd1/agent-os/pull/549"
|
|
1916
|
+
},
|
|
1917
|
+
{
|
|
1918
|
+
"id": 550,
|
|
1919
|
+
"date": "2026-11-01",
|
|
1920
|
+
"kind": "new",
|
|
1921
|
+
"area": "AgentOS",
|
|
1922
|
+
"text": "You can set your name in Settings, and AgentOS uses it in chats and notes instead of a fixed name.",
|
|
1923
|
+
"url": "https://github.com/saadnvd1/agent-os/pull/550"
|
|
1908
1924
|
}
|
|
1909
1925
|
]
|
|
1910
1926
|
}
|
|
@@ -16,3 +16,6 @@ export const clientConfigQuery = queryOptions({
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
export const useClientConfig = () => useQuery(clientConfigQuery);
|
|
19
|
+
|
|
20
|
+
/** The owner's own name, or null until they set one (see lib/owner-label). */
|
|
21
|
+
export const useOwnerName = () => useClientConfig().data?.ownerName ?? null;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
2
|
+
import { clientConfigKeys } from "@/data/client-config/queries";
|
|
3
|
+
|
|
4
|
+
export function useSetOwnerName() {
|
|
5
|
+
const qc = useQueryClient();
|
|
6
|
+
return useMutation({
|
|
7
|
+
mutationFn: async (name: string | null) => {
|
|
8
|
+
const res = await fetch("/api/owner", {
|
|
9
|
+
method: "PUT",
|
|
10
|
+
headers: { "Content-Type": "application/json" },
|
|
11
|
+
body: JSON.stringify({ name }),
|
|
12
|
+
});
|
|
13
|
+
const data = await res.json();
|
|
14
|
+
if (!res.ok) throw new Error(data.error || "Couldn't save the name");
|
|
15
|
+
return data.name as string | null;
|
|
16
|
+
},
|
|
17
|
+
onSuccess: () => qc.invalidateQueries({ queryKey: clientConfigKeys.all }),
|
|
18
|
+
});
|
|
19
|
+
}
|
package/lib/approvals/enable.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
recordAutoApproval,
|
|
12
12
|
setNoApprovals,
|
|
13
13
|
} from "./no-approvals";
|
|
14
|
+
import { ownerUi } from "../owner";
|
|
14
15
|
|
|
15
16
|
// Everything already waiting when the mode goes on is approved: open asks,
|
|
16
17
|
// tool cards in live chats.
|
|
@@ -42,7 +43,7 @@ export async function switchNoApprovals(on: boolean): Promise<void> {
|
|
|
42
43
|
o.workspace_id,
|
|
43
44
|
on
|
|
44
45
|
? "No approvals mode is on: every ask is approved as it's raised, and agents run without permission prompts."
|
|
45
|
-
:
|
|
46
|
+
: `No approvals mode is off: asks wait for ${ownerUi()} again.`,
|
|
46
47
|
"ask"
|
|
47
48
|
);
|
|
48
49
|
}
|
package/lib/artifacts/tools.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
} from "./store";
|
|
22
22
|
import { followLatest } from "../share/follow";
|
|
23
23
|
import { previewHtml } from "./preview";
|
|
24
|
+
import { owner } from "../owner";
|
|
24
25
|
|
|
25
26
|
export const VISUALS_SERVER = "visuals";
|
|
26
27
|
|
|
@@ -218,4 +219,4 @@ When a chart, table, diagram or mockup would say more than prose, build a self-c
|
|
|
218
219
|
// is told what to keep out of a page.
|
|
219
220
|
export const ORCHESTRATOR_VISUALS_BRIEF = `## Pages
|
|
220
221
|
|
|
221
|
-
\`html_render\` shows a self-contained HTML page (a report, instructions) in this chat for
|
|
222
|
+
\`html_render\` shows a self-contained HTML page (a report, instructions) in this chat for ${owner()} to read or share; check it with \`html_preview\` first. Put only what you wrote in it: no secrets, and nothing from a session's text loaded from a URL.`;
|
package/lib/away/brief.ts
CHANGED
|
@@ -12,9 +12,10 @@ import {
|
|
|
12
12
|
} from "./state";
|
|
13
13
|
import { SWEEP_EVERY_MS } from "./sweep";
|
|
14
14
|
import { awayWindowMinutes } from "./activity";
|
|
15
|
+
import { owner, Owner } from "../owner";
|
|
15
16
|
|
|
16
17
|
export function awayOnLine(row: AwayRow, now = Date.now()): string {
|
|
17
|
-
return
|
|
18
|
+
return `${Owner()} is away (Away mode, since ${clockTime(fromSql(row.started_at), now)}, ${untilText(row, now)}). Follow work through rather than only reacting to events: keep each task moving to merged and deployed, sign off what passes its gates, and pick up what stalled. Every ${SWEEP_EVERY_MS / 60000} minutes AgentOS sweeps this workspace without messaging any session: it runs lost reviews again, checks the deploy after each merge, and sends you one digest of what's new (stalled tasks, idle ones with uncommitted work and no PR, ${owner()}'s own sessions idle with uncommitted work). You decide what needs work and act with your tools. Only sessions with real activity in the last ${awayWindowMinutes()} minutes are listed: leave the rest alone, since a message wakes an agent and re-reads its whole context. Interactive sessions (not tasks) are ${owner()}'s: ask a listed one at most once to wrap up, never drive them further. Hard lines still go to ask_owner; while he's away his asks reach his phone in one batch per sweep, so raise them as usual. \`aos notify\` is held for that batch too, so don't send one per item.`;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
// For the digest: the same line whenever Away is on, messaged or not, so an
|
package/lib/away/run.ts
CHANGED
|
@@ -56,6 +56,7 @@ import { checkDeploy } from "./deploy";
|
|
|
56
56
|
import { buildAwayReport, phoneSummary, reportMarkdown } from "./report";
|
|
57
57
|
import { awayOnLine } from "./brief";
|
|
58
58
|
import { shutFinishedTaskSimulators } from "../simulator";
|
|
59
|
+
import { owner, Owner, ownerUi } from "../owner";
|
|
59
60
|
|
|
60
61
|
const PHONE_SOURCE = "away";
|
|
61
62
|
const PHONE_LINES = 8;
|
|
@@ -116,7 +117,7 @@ function tellAway(
|
|
|
116
117
|
).run(away.id, workspaceId, orchestratorId, toldKey(workspaceId));
|
|
117
118
|
addNote(
|
|
118
119
|
workspaceId,
|
|
119
|
-
|
|
120
|
+
`${ownerUi(true)} turned Away on, ${untilText(away, now)}.`,
|
|
120
121
|
"away"
|
|
121
122
|
);
|
|
122
123
|
}
|
|
@@ -181,13 +182,13 @@ export function turnAwayOff(
|
|
|
181
182
|
}
|
|
182
183
|
const line =
|
|
183
184
|
by === "expired"
|
|
184
|
-
?
|
|
185
|
-
:
|
|
185
|
+
? `Away mode ran out: ${owner()} may be back. Go back to reacting to events; don't drive interactive sessions.`
|
|
186
|
+
: `${Owner()} is back: Away mode is off. Go back to reacting to events.`;
|
|
186
187
|
for (const workspaceId of reachedWorkspaces(ended.id)) {
|
|
187
188
|
queueEvent(workspaceId, `away-back:${ended.id}`, null, line, now);
|
|
188
189
|
addNote(
|
|
189
190
|
workspaceId,
|
|
190
|
-
by === "expired" ? "Away ran out." :
|
|
191
|
+
by === "expired" ? "Away ran out." : `${ownerUi(true)} turned Away off.`,
|
|
191
192
|
"away"
|
|
192
193
|
);
|
|
193
194
|
}
|
package/lib/away/sweep.ts
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { isFinished } from "../tasks/state";
|
|
23
23
|
import type { SessionFacts } from "../orchestrator/facts";
|
|
24
|
+
import { owner } from "../owner";
|
|
24
25
|
|
|
25
26
|
export const SWEEP_EVERY_MS = 10 * 60 * 1000;
|
|
26
27
|
export const STALL_MS = 30 * 60 * 1000;
|
|
@@ -199,7 +200,7 @@ function wrapupCandidate(f: SessionFacts, v: SweepView): Candidate | null {
|
|
|
199
200
|
kind: "wrapup",
|
|
200
201
|
sessionId: f.id,
|
|
201
202
|
key,
|
|
202
|
-
line: `${f.name} (${f.id}):
|
|
203
|
+
line: `${f.name} (${f.id}): ${owner()}'s own session, idle ${minutes(v.now - turnOf(f, v))}m with uncommitted work.`,
|
|
203
204
|
};
|
|
204
205
|
}
|
|
205
206
|
|
package/lib/chat/runner.ts
CHANGED
|
@@ -86,6 +86,7 @@ import { DEMO_REFUSAL, demoMode } from "../security/demo";
|
|
|
86
86
|
import { chatHold, chatRefusal } from "./hold";
|
|
87
87
|
import { getHost } from "../hosts";
|
|
88
88
|
import { hostLink } from "../hosts/remote-api";
|
|
89
|
+
import { ownerUi } from "../owner";
|
|
89
90
|
|
|
90
91
|
export {
|
|
91
92
|
refreshCapabilities,
|
|
@@ -119,7 +120,7 @@ export async function carryOutPlan(
|
|
|
119
120
|
{
|
|
120
121
|
id: `user-carry-${planId}`,
|
|
121
122
|
text: CARRY_OUT,
|
|
122
|
-
origin: { kind: "decision", label:
|
|
123
|
+
origin: { kind: "decision", label: ownerUi(true) },
|
|
123
124
|
host: true,
|
|
124
125
|
},
|
|
125
126
|
timeoutMs
|
package/lib/chat/turns.ts
CHANGED
|
@@ -38,7 +38,7 @@ export const isQuestionTool = (item: ChatItem): boolean =>
|
|
|
38
38
|
item.kind === "tool" && /(^|__)question$/.test(item.name);
|
|
39
39
|
|
|
40
40
|
export const isAskTool = (item: ChatItem): boolean =>
|
|
41
|
-
item.kind === "tool" && /(^|__)
|
|
41
|
+
item.kind === "tool" && /(^|__)ask_(owner|saad)$/.test(item.name);
|
|
42
42
|
|
|
43
43
|
// The assistant message a question was asked in (its turn's last words
|
|
44
44
|
// before the call), else the call itself.
|
|
@@ -30,6 +30,8 @@ export interface ClientConfig {
|
|
|
30
30
|
intake: IntakeLimits;
|
|
31
31
|
attachments: AttachmentConfig;
|
|
32
32
|
voice: VoiceLimits;
|
|
33
|
+
/** The owner's own name for text that refers to them; null until set. */
|
|
34
|
+
ownerName: string | null;
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
// What a chat accepts, served from lib/attachments/policy.ts. Sizes in bytes.
|
|
@@ -72,7 +74,9 @@ export interface VoiceLimits {
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
export const INTAKE_LIMITS: IntakeLimits = {
|
|
75
|
-
|
|
77
|
+
// All files together; each is also held to its kind's limit in
|
|
78
|
+
// lib/attachments/policy.ts (text up to maxTextBytes, so a big HAR fits).
|
|
79
|
+
maxBytes: 100_000_000,
|
|
76
80
|
maxFiles: 8,
|
|
77
81
|
maxInlineImages: 2,
|
|
78
82
|
maxNoteChars: 280,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ownerName } from "../owner";
|
|
1
2
|
import { ATTACHMENT_LIMITS } from "../attachments/policy";
|
|
2
3
|
import { ACCESS_LEVELS, DEFAULT_NEW_CHAT_ACCESS } from "../chat/access-levels";
|
|
3
4
|
import { CHAT_ACCESS } from "../chat/events";
|
|
@@ -42,5 +43,6 @@ export function clientConfig(): ClientConfig {
|
|
|
42
43
|
intake: INTAKE_LIMITS,
|
|
43
44
|
attachments: ATTACHMENT_LIMITS,
|
|
44
45
|
voice: VOICE_LIMITS,
|
|
46
|
+
ownerName: ownerName(),
|
|
45
47
|
};
|
|
46
48
|
}
|