@saadnvd1/agent-os 0.3.560 → 0.3.564

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.
Files changed (62) hide show
  1. package/README.md +1 -0
  2. package/app/api/devices/[id]/route.ts +3 -6
  3. package/app/api/devices/me/route.ts +2 -2
  4. package/app/api/presence/device-key/options/route.ts +36 -0
  5. package/app/api/presence/device-key/route.ts +55 -0
  6. package/app/api/presence/enroll/route.ts +7 -15
  7. package/app/api/presence/options/route.ts +20 -0
  8. package/app/api/presence/passkeys/[id]/route.ts +11 -9
  9. package/app/api/shares/[id]/route.ts +23 -0
  10. package/app/api/shares/preview/route.ts +30 -0
  11. package/app/api/shares/route.test.ts +455 -0
  12. package/app/api/shares/route.ts +101 -0
  13. package/app/api/workspaces/[id]/orchestrator/asks/[askId]/route.ts +15 -10
  14. package/app/api/workspaces/[id]/orchestrator/pause/route.ts +12 -9
  15. package/app/api/workspaces/[id]/orchestrator/rules/[ruleId]/route.ts +3 -3
  16. package/app/api/workspaces/[id]/orchestrator/rules/presence.ts +10 -7
  17. package/app/api/workspaces/[id]/orchestrator/rules/route.ts +5 -5
  18. package/app/page.tsx +5 -0
  19. package/components/Chat/Artifact.tsx +17 -1
  20. package/components/Devices/PasskeysSection.tsx +4 -2
  21. package/components/Pane/MobileTabBar.tsx +19 -0
  22. package/components/Pane/PaneMenu.tsx +16 -0
  23. package/components/SessionList/SessionRowMenu.tsx +16 -0
  24. package/components/Settings/SettingsDialog.test.ts +3 -0
  25. package/components/Settings/SettingsDialog.tsx +9 -0
  26. package/components/Settings/commands.ts +9 -0
  27. package/components/Settings/entry-points.test.ts +1 -0
  28. package/components/Share/ShareDialog.tsx +336 -0
  29. package/components/Share/format.test.ts +20 -0
  30. package/components/Share/format.ts +33 -0
  31. package/components/Shares/SharedLinksPanel.tsx +147 -0
  32. package/data/shares/index.ts +2 -0
  33. package/data/shares/keys.ts +4 -0
  34. package/data/shares/queries.ts +111 -0
  35. package/lib/chat/drivers/claude.ts +5 -4
  36. package/lib/connect/control-api.ts +84 -0
  37. package/lib/db/migrations-legacy-checksums.json +90 -0
  38. package/lib/db/migrations.ts +120 -23
  39. package/lib/orchestrator/passkey-asks.ts +14 -0
  40. package/lib/orchestrator/untrusted.ts +12 -2
  41. package/lib/security/demo.ts +1 -0
  42. package/lib/security/device-key-message.ts +27 -0
  43. package/lib/security/device-keys.ts +75 -0
  44. package/lib/security/passkeys.ts +11 -4
  45. package/lib/security/presence-http.ts +49 -1
  46. package/lib/security/presence.ts +184 -0
  47. package/lib/security/soft-authenticator.ts +21 -0
  48. package/lib/session-audit/normalize.ts +1 -1
  49. package/lib/settings-url.ts +1 -0
  50. package/lib/share/connect.ts +86 -0
  51. package/lib/share/context.ts +50 -0
  52. package/lib/share/redact.ts +190 -0
  53. package/lib/share/render.tsx +161 -0
  54. package/lib/share/route.ts +33 -0
  55. package/lib/share/service.ts +159 -0
  56. package/lib/share/snapshot.ts +261 -0
  57. package/lib/share/static-markup.ts +16 -0
  58. package/lib/share/store.ts +39 -0
  59. package/lib/share/styles.ts +83 -0
  60. package/lib/sidebar/shelves.ts +10 -5
  61. package/package.json +1 -1
  62. package/stores/shareUi.ts +17 -0
package/README.md CHANGED
@@ -20,6 +20,7 @@ and check on all of it from your phone.
20
20
  - **Built for your phone.** The whole app works on a phone; pair it with a QR code. [More](docs/guides/phone.md)
21
21
  - **Agents that talk to each other.** Sessions message each other with `aos`, and you see every conversation in Messages. [More](docs/guides/agent-network.md)
22
22
  - **Chat or terminal, same conversation.** Streaming replies, steps and diffs, plan mode, slash commands, `@file`, undo. [More](docs/guides/chat.md)
23
+ - **Share a chat or a page.** A read-only snapshot link with secrets, home paths and machine names masked, and a preview before anything leaves your machine. [More](docs/guides/sharing.md)
23
24
  - **Project config.** `agentos.json` gives each session its own ports, environment, files and a private Postgres copy. [More](docs/reference/project-config.md)
24
25
  - **Schedules and recipes.** Start work on a timer, or save a job that comes up again. [More](docs/guides/schedules.md)
25
26
  - **Plans in pieces and stacks.** A plan splits into several PRs only for a real reason; a board can run as stacked PRs. [More](docs/guides/tasks.md#plans-in-pieces)
@@ -1,9 +1,6 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import {
3
- renameDevice,
4
- revokeDevice,
5
- setDeviceCanApprove,
6
- } from "@/lib/security/devices";
2
+ import { renameDevice, setDeviceCanApprove } from "@/lib/security/devices";
3
+ import { revokeDeviceAndKeys } from "@/lib/orchestrator/passkey-asks";
7
4
  import { requireLocalTrust } from "@/lib/security/route-guard";
8
5
  import { DEVICE_HEADER } from "@/lib/security/auth";
9
6
  import { demoMode } from "@/lib/security/demo";
@@ -43,7 +40,7 @@ export async function DELETE(request: NextRequest, { params }: Ctx) {
43
40
  { error: "Not available in the demo." },
44
41
  { status: 403 }
45
42
  );
46
- if (!revokeDevice(id))
43
+ if (!revokeDeviceAndKeys(id, "Devices"))
47
44
  return NextResponse.json({ error: "Not found" }, { status: 404 });
48
45
  return NextResponse.json({ ok: true });
49
46
  }
@@ -1,12 +1,12 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import { revokeDevice } from "@/lib/security/devices";
2
+ import { revokeDeviceAndKeys } from "@/lib/orchestrator/passkey-asks";
3
3
  import { callingDevice, needsDevice } from "@/lib/security/device-me";
4
4
 
5
5
  // DELETE /api/devices/me - the device signs itself out ("Forget machine")
6
6
  export async function DELETE(request: NextRequest) {
7
7
  const id = callingDevice(request);
8
8
  if (!id) return needsDevice();
9
- if (!revokeDevice(id))
9
+ if (!revokeDeviceAndKeys(id, "the device itself"))
10
10
  return NextResponse.json({ error: "Not found" }, { status: 404 });
11
11
  return NextResponse.json({ ok: true });
12
12
  }
@@ -0,0 +1,36 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { requireApprover } from "@/lib/security/approver";
3
+ import {
4
+ deviceRegistrationOptions,
5
+ PresenceError,
6
+ } from "@/lib/security/presence";
7
+ import { refusalResponse } from "@/lib/security/presence-http";
8
+ import { passkeysBootstrapped } from "@/lib/security/passkeys";
9
+
10
+ // A challenge for this paired device's new approval key to sign. The key is
11
+ // the device's own, so only a paired device asks; with passkeys already set
12
+ // up, it needs a code from one of them, like a browser passkey does.
13
+ export async function POST(request: NextRequest) {
14
+ const gate = requireApprover(request);
15
+ if (!gate.ok) return gate.response;
16
+ if (gate.approver.via !== "device" || !gate.approver.deviceId)
17
+ return NextResponse.json(
18
+ { error: "Only a paired device has its own key" },
19
+ { status: 403 }
20
+ );
21
+ try {
22
+ const { enrollCode } = (await request.json().catch(() => ({}))) as {
23
+ enrollCode?: string;
24
+ };
25
+ return NextResponse.json(
26
+ deviceRegistrationOptions(gate.approver.deviceId, enrollCode)
27
+ );
28
+ } catch (error) {
29
+ if (error instanceof PresenceError && error.code === "enroll")
30
+ return NextResponse.json({
31
+ needsCode: true,
32
+ canBootstrap: !passkeysBootstrapped(),
33
+ });
34
+ return refusalResponse(error);
35
+ }
36
+ }
@@ -0,0 +1,55 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { requireApprover } from "@/lib/security/approver";
3
+ import { verifyDeviceRegistration } from "@/lib/security/presence";
4
+ import { placeOf, refusalResponse } from "@/lib/security/presence-http";
5
+ import { raisePasskeyAsks } from "@/lib/orchestrator/passkey-asks";
6
+ import { resolvePasskeyAsks } from "@/lib/orchestrator/asks";
7
+
8
+ // Stores the paired device's own approval key, and puts it on Saad's asks
9
+ // list to confirm. A key it replaces (a reinstall) is revoked and noted.
10
+ export async function POST(request: NextRequest) {
11
+ const gate = requireApprover(request);
12
+ if (!gate.ok) return gate.response;
13
+ if (gate.approver.via !== "device" || !gate.approver.deviceId)
14
+ return NextResponse.json(
15
+ { error: "Only a paired device has its own key" },
16
+ { status: 403 }
17
+ );
18
+ try {
19
+ const body = (await request.json().catch(() => ({}))) as {
20
+ publicKey?: string;
21
+ challenge?: string;
22
+ signature?: string;
23
+ name?: string;
24
+ hardware?: string;
25
+ enrollCode?: string;
26
+ };
27
+ if (!body.publicKey || !body.challenge || !body.signature)
28
+ return NextResponse.json({ error: "No key" }, { status: 400 });
29
+ const place = placeOf(gate.approver, request.headers);
30
+ const { key, replaced } = verifyDeviceRegistration(
31
+ gate.approver.deviceId,
32
+ {
33
+ publicKey: body.publicKey,
34
+ challenge: body.challenge,
35
+ signature: body.signature,
36
+ name: body.name || place,
37
+ hardware: body.hardware,
38
+ },
39
+ {
40
+ via: gate.approver.via,
41
+ from: place,
42
+ userAgent: request.headers.get("user-agent"),
43
+ },
44
+ body.enrollCode
45
+ );
46
+ for (const old of replaced) {
47
+ resolvePasskeyAsks(old.id, "replaced");
48
+ raisePasskeyAsks(old, place, "revoked");
49
+ }
50
+ raisePasskeyAsks(key, place);
51
+ return NextResponse.json({ passkey: { id: key.id, name: key.name } });
52
+ } catch (error) {
53
+ return refusalResponse(error);
54
+ }
55
+ }
@@ -1,12 +1,11 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import type { AuthenticationResponseJSON } from "@simplewebauthn/server";
3
2
  import { requireApprover } from "@/lib/security/approver";
3
+ import { mintEnrollCode } from "@/lib/security/presence";
4
4
  import {
5
- mintEnrollCode,
6
- relyingParty,
7
- verifyPresence,
8
- } from "@/lib/security/presence";
9
- import { refusalResponse } from "@/lib/security/presence-http";
5
+ refusalResponse,
6
+ requirePresence,
7
+ type PresenceProof,
8
+ } from "@/lib/security/presence-http";
10
9
 
11
10
  // A one-time code (10 minutes) for adding a passkey on another device,
12
11
  // minted only on an assertion from an existing passkey.
@@ -14,15 +13,8 @@ export async function POST(request: NextRequest) {
14
13
  const gate = requireApprover(request);
15
14
  if (!gate.ok) return gate.response;
16
15
  try {
17
- const { assertion } = (await request.json().catch(() => ({}))) as {
18
- assertion?: AuthenticationResponseJSON;
19
- };
20
- await verifyPresence(
21
- relyingParty(request.headers),
22
- assertion,
23
- "enroll",
24
- "enroll"
25
- );
16
+ const body = (await request.json().catch(() => ({}))) as PresenceProof;
17
+ await requirePresence(request, gate.approver, body, "enroll", "enroll");
26
18
  return NextResponse.json({ code: mintEnrollCode() });
27
19
  } catch (error) {
28
20
  return refusalResponse(error);
@@ -2,6 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
2
2
  import { requireApprover } from "@/lib/security/approver";
3
3
  import {
4
4
  assertionOptions,
5
+ deviceAssertionOptions,
5
6
  relyingParty,
6
7
  type PresencePurpose,
7
8
  } from "@/lib/security/presence";
@@ -21,8 +22,27 @@ export async function POST(request: NextRequest) {
21
22
  askId?: number;
22
23
  binding?: string;
23
24
  passkeyId?: string;
25
+ method?: "device";
24
26
  };
25
27
  const binding = presenceBinding(body);
28
+ if (body.method === "device") {
29
+ if (gate.approver.via !== "device" || !gate.approver.deviceId)
30
+ return NextResponse.json(
31
+ { error: "Only a paired device has its own key" },
32
+ { status: 403 }
33
+ );
34
+ const device = deviceAssertionOptions(
35
+ gate.approver.deviceId,
36
+ body.purpose!,
37
+ binding
38
+ );
39
+ if (!device)
40
+ return NextResponse.json({
41
+ needsDeviceKey: true,
42
+ canBootstrap: !passkeysBootstrapped(),
43
+ });
44
+ return NextResponse.json({ device });
45
+ }
26
46
  const rp = relyingParty(request.headers);
27
47
  const options = await assertionOptions(rp, body.purpose!, binding);
28
48
  if (!options)
@@ -1,9 +1,12 @@
1
1
  import { NextRequest, NextResponse } from "next/server";
2
- import type { AuthenticationResponseJSON } from "@simplewebauthn/server";
3
2
  import { requireApprover } from "@/lib/security/approver";
4
3
  import { getPasskey, revokePasskey } from "@/lib/security/passkeys";
5
- import { relyingParty, verifyPresence } from "@/lib/security/presence";
6
- import { placeOf, refusalResponse } from "@/lib/security/presence-http";
4
+ import {
5
+ placeOf,
6
+ refusalResponse,
7
+ requirePresence,
8
+ type PresenceProof,
9
+ } from "@/lib/security/presence-http";
7
10
  import { raisePasskeyAsks } from "@/lib/orchestrator/passkey-asks";
8
11
  import { revokePresence } from "@/lib/orchestrator/presence-binding";
9
12
  import { resolvePasskeyAsks } from "@/lib/orchestrator/asks";
@@ -21,12 +24,11 @@ export async function DELETE(request: NextRequest, { params }: Ctx) {
21
24
  const key = getPasskey(id);
22
25
  if (!key || key.revoked_at)
23
26
  return NextResponse.json({ error: "Not found" }, { status: 404 });
24
- const { assertion } = (await request.json().catch(() => ({}))) as {
25
- assertion?: AuthenticationResponseJSON;
26
- };
27
- await verifyPresence(
28
- relyingParty(request.headers),
29
- assertion,
27
+ const body = (await request.json().catch(() => ({}))) as PresenceProof;
28
+ await requirePresence(
29
+ request,
30
+ gate.approver,
31
+ body,
30
32
  "revoke",
31
33
  revokePresence(id)
32
34
  );
@@ -0,0 +1,23 @@
1
+ import { NextResponse } from "next/server";
2
+ import { NOT_LINKED, linked, revokeRemote } from "@/lib/share/connect";
3
+ import { fail, refuseDemo, toResponse } from "@/lib/share/route";
4
+ import { getSharedLink, markRevoked } from "@/lib/share/store";
5
+
6
+ // DELETE /api/shares/[id] - revoke a link: Connect deletes the page at once.
7
+ export async function DELETE(
8
+ _request: Request,
9
+ { params }: { params: Promise<{ id: string }> }
10
+ ) {
11
+ const refused = refuseDemo();
12
+ if (refused) return refused;
13
+ const { id } = await params;
14
+ if (!getSharedLink(id)) return fail("No such shared link.", 404);
15
+ if (!linked()) return fail(NOT_LINKED, 409);
16
+ try {
17
+ await revokeRemote(id);
18
+ markRevoked(id);
19
+ return NextResponse.json({ ok: true });
20
+ } catch (err) {
21
+ return toResponse(err);
22
+ }
23
+ }
@@ -0,0 +1,30 @@
1
+ import { NextResponse } from "next/server";
2
+ import { linked } from "@/lib/share/connect";
3
+ import { readJson, refuseDemo, toResponse } from "@/lib/share/route";
4
+ import { buildPage, parseRequest } from "@/lib/share/service";
5
+
6
+ // POST /api/shares/preview - exactly what a share would upload, redacted.
7
+ // Local work only: nothing goes to Connect until a share is created.
8
+ export async function POST(request: Request) {
9
+ const refused = refuseDemo();
10
+ if (refused) return refused;
11
+ const read = await readJson(request);
12
+ if ("refused" in read) return read.refused;
13
+ try {
14
+ const req = parseRequest(read.body);
15
+ const page = await buildPage(req);
16
+ return NextResponse.json({
17
+ html: page.html,
18
+ title: page.title,
19
+ bytes: page.bytes,
20
+ hash: page.hash,
21
+ sharedAt: req.sharedAt,
22
+ items: page.items,
23
+ hits: page.hits,
24
+ tooBig: page.tooBig,
25
+ connectLinked: linked(),
26
+ });
27
+ } catch (err) {
28
+ return toResponse(err);
29
+ }
30
+ }