@cueloop/daemon 0.1.0-alpha.9 → 0.1.0-alpha.91

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 (60) hide show
  1. package/package.json +39 -22
  2. package/src/api.test.ts +1060 -65
  3. package/src/api.ts +1398 -89
  4. package/src/capabilities.test.ts +57 -0
  5. package/src/capabilities.ts +89 -0
  6. package/src/client.test.ts +24 -0
  7. package/src/client.ts +669 -86
  8. package/src/curate.ts +109 -0
  9. package/src/daemon-version.test.ts +142 -0
  10. package/src/diff-hot-reload.test.ts +419 -0
  11. package/src/diff-watcher.ts +293 -0
  12. package/src/gh.test.ts +59 -0
  13. package/src/gh.ts +74 -0
  14. package/src/ghostty-thread-surface-store.test.ts +49 -0
  15. package/src/ghostty-thread-surface-store.ts +101 -0
  16. package/src/ghostty-thread-surface.test.ts +398 -0
  17. package/src/ghostty-thread-surface.ts +267 -0
  18. package/src/harness-delivery.test.ts +268 -0
  19. package/src/harness-state-store.test.ts +93 -0
  20. package/src/harness-state-store.ts +213 -0
  21. package/src/herdr-split.test.ts +106 -0
  22. package/src/herdr-split.ts +92 -0
  23. package/src/herdr-thread-surface-store.test.ts +81 -0
  24. package/src/herdr-thread-surface-store.ts +64 -0
  25. package/src/herdr-thread-surface.test.ts +411 -0
  26. package/src/herdr-thread-surface.ts +355 -0
  27. package/src/index.ts +8 -9
  28. package/src/interruptible-wait.test.ts +127 -0
  29. package/src/interruptible-wait.ts +66 -0
  30. package/src/jj-working-tree.ts +131 -0
  31. package/src/main.ts +4 -1
  32. package/src/paths.ts +49 -0
  33. package/src/pr-poller.test.ts +102 -0
  34. package/src/pr-poller.ts +73 -0
  35. package/src/project-files.test.ts +124 -0
  36. package/src/project-files.ts +56 -0
  37. package/src/protocol.ts +79 -7
  38. package/src/retention.test.ts +191 -0
  39. package/src/retention.ts +123 -0
  40. package/src/server.test.ts +453 -42
  41. package/src/server.ts +433 -88
  42. package/src/share-blob.test.ts +112 -0
  43. package/src/share-blob.ts +55 -0
  44. package/src/single-instance.test.ts +162 -13
  45. package/src/store.conformance.test.ts +41 -0
  46. package/src/store.jsonl.test.ts +159 -0
  47. package/src/store.ts +264 -34
  48. package/src/testing/store-conformance.ts +194 -0
  49. package/src/thread-review.test.ts +483 -0
  50. package/src/thread-review.ts +398 -0
  51. package/src/thread-surface-config.test.ts +63 -0
  52. package/src/thread-surface-config.ts +61 -0
  53. package/src/validate.test.ts +282 -37
  54. package/src/validate.ts +398 -33
  55. package/src/vcs-source.test.ts +180 -0
  56. package/src/vcs-source.ts +228 -0
  57. package/src/version.ts +6 -0
  58. package/src/working-tree.ts +149 -0
  59. package/src/review.test.ts +0 -167
  60. package/src/review.ts +0 -174
@@ -0,0 +1,57 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { asDaemonRole, DEFAULT_ROLE, roleAllowsMethod } from "./capabilities";
3
+ import type { MethodName } from "./validate";
4
+
5
+ describe("roleAllowsMethod", () => {
6
+ test("the owner may call every primitive", () => {
7
+ // Assert
8
+ const owned: MethodName[] = ["session.sendMessage", "session.submitRevision", "session.delete"];
9
+
10
+ for (const method of owned) expect(roleAllowsMethod("owner", method)).toBe(true);
11
+ });
12
+
13
+ test("a capped role may read and annotate but not escalate", () => {
14
+ // Assert - allowed
15
+ const open: MethodName[] = ["session.get", "session.list", "session.wait", "session.annotate"];
16
+
17
+ for (const method of open) expect(roleAllowsMethod("agent", method)).toBe(true);
18
+ // Assert - denied
19
+ const owned: MethodName[] = [
20
+ "session.sendMessage",
21
+ "session.submitRevision",
22
+ "session.setWorkingCopy",
23
+ "session.delete",
24
+ "session.setShareId",
25
+ "session.refreshDiff",
26
+ "session.create",
27
+ "session.navigate",
28
+ "session.branch",
29
+ "session.switch",
30
+ "session.label",
31
+ "session.fork",
32
+ ];
33
+
34
+ for (const method of owned) expect(roleAllowsMethod("agent", method)).toBe(false);
35
+ expect(roleAllowsMethod("collaborator", "session.sendMessage")).toBe(false);
36
+ // removal and naming are open to every role; the primitive itself scopes a
37
+ // non-owner to the author it acts as
38
+ expect(roleAllowsMethod("agent", "session.removeAnnotation")).toBe(true);
39
+ expect(roleAllowsMethod("agent", "session.setParticipantName")).toBe(true);
40
+ });
41
+
42
+ test("a connection starts as a collaborator, never as the owner", () => {
43
+ // Assert
44
+ expect(DEFAULT_ROLE).toBe("collaborator");
45
+ });
46
+ });
47
+
48
+ describe("asDaemonRole", () => {
49
+ test("passes owner and collaborator through and caps anything unknown to agent", () => {
50
+ // Assert
51
+ expect(asDaemonRole("owner")).toBe("owner");
52
+ expect(asDaemonRole("collaborator")).toBe("collaborator");
53
+ expect(asDaemonRole("agent")).toBe("agent");
54
+ expect(asDaemonRole("root")).toBe("agent");
55
+ expect(asDaemonRole(undefined)).toBe("agent");
56
+ });
57
+ });
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Daemon roles: which primitives a connection may call. A connection is a
3
+ * collaborator until it proves ownership with the owner token in daemon.hello
4
+ * - never an owner by default. The owner (the local planner) may call every
5
+ * primitive; a collaborator or a review-side agent is capped to reading the
6
+ * session and adding comments, so a bring-your-own agent cannot resolve,
7
+ * rewrite, cut, share, or delete (ADR 0009). The table below is exhaustive
8
+ * over the primitive schemas: adding a primitive without naming its roles
9
+ * does not compile.
10
+ */
11
+
12
+ import * as v from "valibot";
13
+ import type { MethodName } from "./validate";
14
+
15
+ export type DaemonRole = "owner" | "collaborator" | "agent";
16
+
17
+ /** Who may call a primitive: everyone, or the owner only. */
18
+ type Audience = "any" | "owner";
19
+
20
+ const PRIMITIVE_ROLES = {
21
+ "daemon.ping": "any",
22
+ "daemon.hello": "any",
23
+ "daemon.shutdown": "owner",
24
+ "events.subscribe": "any",
25
+ "session.create": "owner",
26
+ "session.get": "any",
27
+ "session.list": "any",
28
+ "session.wait": "any",
29
+ "session.annotate": "any",
30
+ "session.comment": "any",
31
+ // any role may call it; a non-owner must act on behalf of an author and stays scoped to it
32
+ "session.removeAnnotation": "any",
33
+ "session.setParticipantName": "any",
34
+ "session.setWorkingCopy": "owner",
35
+ "session.cutBlock": "owner",
36
+ "session.restoreBlock": "owner",
37
+ "session.curate": "owner",
38
+ "session.setAccess": "owner",
39
+ "session.setViewed": "owner",
40
+ "session.setTitle": "owner",
41
+ // reading the local working tree is the owner's alone; a share carries no repo to read
42
+ "session.projectFiles": "owner",
43
+ "session.fileContents": "owner",
44
+ // the launch repo is the local owner's; a share has no local repo to browse
45
+ "repo.files": "owner",
46
+ "repo.fileContents": "owner",
47
+ "repo.changes": "owner",
48
+ "repo.diff": "owner",
49
+ "session.workbench": "owner",
50
+ // the tree is the owner's: collaborators comment on the branch a share follows
51
+ "session.navigate": "owner",
52
+ "session.branch": "owner",
53
+ "session.switch": "owner",
54
+ "session.label": "owner",
55
+ "session.fork": "owner",
56
+ "session.refreshDiff": "owner",
57
+ "session.setShareId": "owner",
58
+ "session.setShares": "owner",
59
+ "session.delete": "owner",
60
+ "session.mergeShared": "owner",
61
+ "session.sendMessage": "owner",
62
+ "harness.bind": "owner",
63
+ "harness.getBinding": "owner",
64
+ "harness.bindingsForSession": "owner",
65
+ "harness.consumeApprovedRetry": "owner",
66
+ "delivery.pending": "owner",
67
+ "delivery.acknowledge": "owner",
68
+ "session.submitRevision": "owner",
69
+ "herdr.getThreadSurface": "owner",
70
+ "herdr.setThreadSurface": "owner",
71
+ "ghostty.getThreadSurface": "owner",
72
+ "ghostty.setThreadSurface": "owner",
73
+ "ghostty.claimThreadSurface": "owner",
74
+ "ghostty.releaseThreadSurface": "owner",
75
+ } as const satisfies Record<MethodName, Audience>;
76
+
77
+ /** Whether `role` may call `method`. */
78
+ export function roleAllowsMethod(role: DaemonRole, method: MethodName): boolean {
79
+ return role === "owner" || PRIMITIVE_ROLES[method] === "any";
80
+ }
81
+
82
+ /** The role a connection starts with, before any proof. */
83
+ export const DEFAULT_ROLE: DaemonRole = "collaborator";
84
+
85
+ export function asDaemonRole(value: Parameters<typeof v.safeParse>[1]): DaemonRole {
86
+ const result = v.safeParse(v.picklist(["owner", "collaborator"]), value);
87
+
88
+ return result.success ? result.output : "agent";
89
+ }
@@ -0,0 +1,24 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { daemonSpawnCommand } from "./client";
3
+
4
+ describe("daemonSpawnCommand", () => {
5
+ test("re-execs the binary as `daemon` when compiled (bun virtual-fs markers)", () => {
6
+ for (const moduleUrl of [
7
+ "file:///$bunfs/root/cueloop",
8
+ "file:///B:/~BUN/root/cueloop",
9
+ "file:///B:/%7EBUN/root/cueloop",
10
+ ]) {
11
+ expect(daemonSpawnCommand("/usr/local/bin/cueloop", moduleUrl)).toEqual([
12
+ "/usr/local/bin/cueloop",
13
+ "daemon",
14
+ "--autostart",
15
+ ]);
16
+ }
17
+ });
18
+
19
+ test("runs main.ts with bun from source", () => {
20
+ expect(
21
+ daemonSpawnCommand("/path/to/bun", "file:///repo/packages/daemon/src/client.ts"),
22
+ ).toEqual(["/path/to/bun", "run", "/repo/packages/daemon/src/main.ts"]);
23
+ });
24
+ });