@cueloop/daemon 0.1.0-alpha.3 → 0.1.0-alpha.30
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/package.json +13 -5
- package/src/api.test.ts +223 -44
- package/src/api.ts +128 -50
- package/src/client.ts +76 -29
- package/src/herdr-pane.test.ts +146 -0
- package/src/herdr-pane.ts +95 -0
- package/src/index.ts +8 -8
- package/src/protocol.ts +44 -7
- package/src/review.test.ts +243 -0
- package/src/review.ts +208 -0
- package/src/server.test.ts +135 -20
- package/src/server.ts +70 -48
- package/src/share-blob.test.ts +62 -0
- package/src/share-blob.ts +48 -0
- package/src/single-instance.test.ts +28 -11
- package/src/store.ts +5 -5
- package/src/validate.test.ts +152 -12
- package/src/validate.ts +93 -32
package/src/validate.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Runtime validation of the wire boundary
|
|
2
|
+
* Runtime validation of the wire boundary. Everything arriving over the
|
|
3
3
|
* socket is untrusted JSON: adapters, the CLI, extensions, and any script a
|
|
4
4
|
* user writes all speak this protocol. Validating here means DaemonCore only
|
|
5
5
|
* ever sees well-formed input, and a malformed request gets a precise error
|
|
@@ -10,31 +10,51 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import * as v from "valibot";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
SCHEMA_VERSION,
|
|
15
|
+
type Anchor,
|
|
16
|
+
type Annotation,
|
|
17
|
+
type Artifact,
|
|
18
|
+
type ArtifactMeta,
|
|
19
|
+
type Identity,
|
|
20
|
+
type ReviewSession,
|
|
21
|
+
type Revision,
|
|
22
|
+
type Verdict,
|
|
23
|
+
type WorkspaceKey,
|
|
24
|
+
} from "@cueloop/schema";
|
|
14
25
|
import { DaemonError } from "./errors";
|
|
15
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Drift guard for every hand-mirrored shape below. v.object strips keys it
|
|
29
|
+
* does not know, so a schema missing a field silently drops that field on
|
|
30
|
+
* the way into DaemonCore. Each entries object is checked with
|
|
31
|
+
* `satisfies EntriesOf<T>`: adding a field to the schema types without
|
|
32
|
+
* mirroring it here (or mirroring a key that does not exist) fails typecheck.
|
|
33
|
+
*/
|
|
34
|
+
type EntriesOf<T> = { [K in keyof T]-?: v.GenericSchema<any, any> };
|
|
35
|
+
|
|
16
36
|
const NonEmpty = v.pipe(v.string(), v.minLength(1));
|
|
17
37
|
|
|
18
38
|
export const WorkspaceSchema = v.object({
|
|
19
39
|
repoRoot: NonEmpty,
|
|
20
40
|
branch: NonEmpty,
|
|
21
|
-
});
|
|
41
|
+
} satisfies EntriesOf<WorkspaceKey>);
|
|
42
|
+
|
|
43
|
+
export const ArtifactMetaSchema = v.object({
|
|
44
|
+
cwd: v.optional(v.string()),
|
|
45
|
+
agent: v.optional(v.string()),
|
|
46
|
+
agentSessionId: v.optional(v.string()),
|
|
47
|
+
planPath: v.optional(v.string()),
|
|
48
|
+
pr: v.optional(v.string()),
|
|
49
|
+
herdrPane: v.optional(v.string()),
|
|
50
|
+
title: v.optional(v.string()),
|
|
51
|
+
} satisfies EntriesOf<ArtifactMeta>);
|
|
22
52
|
|
|
23
53
|
export const ArtifactSchema = v.object({
|
|
24
54
|
type: v.picklist(["plan", "diff"]),
|
|
25
55
|
content: v.string(),
|
|
26
|
-
meta: v.optional(
|
|
27
|
-
|
|
28
|
-
cwd: v.optional(v.string()),
|
|
29
|
-
agent: v.optional(v.string()),
|
|
30
|
-
agentSessionId: v.optional(v.string()),
|
|
31
|
-
planPath: v.optional(v.string()),
|
|
32
|
-
title: v.optional(v.string()),
|
|
33
|
-
pr: v.optional(v.string()),
|
|
34
|
-
}),
|
|
35
|
-
{},
|
|
36
|
-
),
|
|
37
|
-
});
|
|
56
|
+
meta: v.optional(ArtifactMetaSchema, {}),
|
|
57
|
+
} satisfies EntriesOf<Artifact>);
|
|
38
58
|
|
|
39
59
|
export const AnchorSchema = v.object({
|
|
40
60
|
quote: v.string(),
|
|
@@ -43,19 +63,37 @@ export const AnchorSchema = v.object({
|
|
|
43
63
|
blockIndex: v.optional(v.number()),
|
|
44
64
|
start: v.optional(v.number()),
|
|
45
65
|
end: v.optional(v.number()),
|
|
46
|
-
});
|
|
66
|
+
} satisfies EntriesOf<Anchor>);
|
|
47
67
|
|
|
68
|
+
/** Wire annotations arrive without createdAt - the daemon stamps it. */
|
|
48
69
|
export const AnnotationSchema = v.object({
|
|
49
70
|
id: NonEmpty,
|
|
50
|
-
/** Open kind set
|
|
71
|
+
/** Open kind set: built-ins are comment and suggestion. */
|
|
51
72
|
kind: NonEmpty,
|
|
52
73
|
anchor: AnchorSchema,
|
|
53
74
|
body: v.string(),
|
|
54
75
|
orphan: v.optional(v.boolean()),
|
|
55
|
-
|
|
76
|
+
author: v.optional(v.string()),
|
|
77
|
+
resolution: v.optional(
|
|
78
|
+
v.object({
|
|
79
|
+
revision: v.number(),
|
|
80
|
+
source: v.picklist(["agent", "drift"]),
|
|
81
|
+
}),
|
|
82
|
+
),
|
|
83
|
+
} satisfies EntriesOf<Omit<Annotation, "createdAt">>);
|
|
56
84
|
|
|
57
85
|
const SessionId = NonEmpty;
|
|
58
86
|
|
|
87
|
+
/** A stored annotation: the wire shape plus the daemon-stamped createdAt. */
|
|
88
|
+
export const FullAnnotationSchema = v.object({ ...AnnotationSchema.entries, createdAt: v.string() } satisfies EntriesOf<Annotation>);
|
|
89
|
+
|
|
90
|
+
export const IdentitySchema = v.object({
|
|
91
|
+
id: NonEmpty,
|
|
92
|
+
provider: v.literal("ssh"),
|
|
93
|
+
name: v.optional(v.string()),
|
|
94
|
+
handle: v.optional(v.string()),
|
|
95
|
+
} satisfies EntriesOf<Identity>);
|
|
96
|
+
|
|
59
97
|
export const Params = {
|
|
60
98
|
"session.create": v.object({ workspace: WorkspaceSchema, artifact: ArtifactSchema }),
|
|
61
99
|
"session.get": v.object({ id: SessionId }),
|
|
@@ -75,12 +113,25 @@ export const Params = {
|
|
|
75
113
|
"session.annotate": v.object({ id: SessionId, annotation: AnnotationSchema }),
|
|
76
114
|
"session.removeAnnotation": v.object({ id: SessionId, annotationId: NonEmpty }),
|
|
77
115
|
"session.setWorkingCopy": v.object({ id: SessionId, workingCopy: v.optional(v.string()) }),
|
|
116
|
+
"session.setViewed": v.object({ id: SessionId, viewedPaths: v.array(v.string()) }),
|
|
117
|
+
"session.setShareId": v.object({ id: SessionId, shareId: NonEmpty }),
|
|
118
|
+
"session.delete": v.object({ id: SessionId }),
|
|
119
|
+
"session.mergeShared": v.object({
|
|
120
|
+
id: SessionId,
|
|
121
|
+
annotations: v.array(FullAnnotationSchema),
|
|
122
|
+
participants: v.optional(v.array(IdentitySchema)),
|
|
123
|
+
}),
|
|
78
124
|
"session.resolve": v.object({
|
|
79
125
|
id: SessionId,
|
|
80
126
|
verdictKind: v.picklist(["comment", "approve", "request_changes"]),
|
|
81
127
|
summary: v.optional(v.string(), ""),
|
|
82
128
|
}),
|
|
83
|
-
"session.submitRevision": v.object({
|
|
129
|
+
"session.submitRevision": v.object({
|
|
130
|
+
id: SessionId,
|
|
131
|
+
content: v.string(),
|
|
132
|
+
/** Annotation ids the agent acted on; each is marked addressed. */
|
|
133
|
+
addressedAnnotationIds: v.optional(v.array(NonEmpty), []),
|
|
134
|
+
}),
|
|
84
135
|
"events.subscribe": v.object({}),
|
|
85
136
|
"daemon.ping": v.object({}),
|
|
86
137
|
"daemon.shutdown": v.object({}),
|
|
@@ -97,37 +148,47 @@ export function parseParams<M extends MethodName>(method: M, params: unknown): v
|
|
|
97
148
|
const result = v.safeParse(Params[method], params ?? {});
|
|
98
149
|
if (!result.success) {
|
|
99
150
|
const issue = result.issues[0]!;
|
|
100
|
-
const path = issue.path?.map((
|
|
151
|
+
const path = issue.path?.map((pathSegment) => String(pathSegment.key)).join(".") ?? "";
|
|
101
152
|
throw new DaemonError("invalid_params", `${method}: ${path ? path + " - " : ""}${issue.message}`);
|
|
102
153
|
}
|
|
103
154
|
return result.output;
|
|
104
155
|
}
|
|
105
156
|
|
|
157
|
+
export const RevisionSchema = v.object({
|
|
158
|
+
revision: v.number(),
|
|
159
|
+
content: v.string(),
|
|
160
|
+
submittedAt: v.string(),
|
|
161
|
+
} satisfies EntriesOf<Revision>);
|
|
162
|
+
|
|
163
|
+
export const VerdictSchema = v.object({
|
|
164
|
+
kind: v.picklist(["comment", "approve", "request_changes"]),
|
|
165
|
+
summary: v.string(),
|
|
166
|
+
feedback: v.string(),
|
|
167
|
+
resolvedAt: v.string(),
|
|
168
|
+
} satisfies EntriesOf<Verdict>);
|
|
169
|
+
|
|
106
170
|
/** Persisted records are validated on recovery: a bad file is skipped, not fatal. */
|
|
107
171
|
export const SessionRecordSchema = v.object({
|
|
108
172
|
schemaVersion: v.literal(SCHEMA_VERSION),
|
|
109
173
|
id: NonEmpty,
|
|
110
174
|
workspace: WorkspaceSchema,
|
|
111
175
|
artifact: ArtifactSchema,
|
|
112
|
-
revisions: v.array(
|
|
113
|
-
annotations: v.array(
|
|
176
|
+
revisions: v.array(RevisionSchema),
|
|
177
|
+
annotations: v.array(FullAnnotationSchema),
|
|
114
178
|
workingCopy: v.optional(v.string()),
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
kind: v.picklist(["comment", "approve", "request_changes"]),
|
|
118
|
-
summary: v.string(),
|
|
119
|
-
feedback: v.string(),
|
|
120
|
-
resolvedAt: v.string(),
|
|
121
|
-
}),
|
|
122
|
-
),
|
|
179
|
+
viewedPaths: v.optional(v.array(v.string())),
|
|
180
|
+
verdict: v.nullable(VerdictSchema),
|
|
123
181
|
status: v.picklist(["pending", "resolved"]),
|
|
124
182
|
createdAt: v.string(),
|
|
125
|
-
|
|
183
|
+
shareId: v.optional(v.string()),
|
|
184
|
+
owner: v.optional(v.string()),
|
|
185
|
+
participants: v.optional(v.array(IdentitySchema)),
|
|
186
|
+
} satisfies EntriesOf<ReviewSession>);
|
|
126
187
|
|
|
127
188
|
export function validateSessionRecord(raw: unknown): { ok: true; value: v.InferOutput<typeof SessionRecordSchema> } | { ok: false; error: string } {
|
|
128
189
|
const result = v.safeParse(SessionRecordSchema, raw);
|
|
129
190
|
if (result.success) return { ok: true, value: result.output };
|
|
130
191
|
const issue = result.issues[0]!;
|
|
131
|
-
const path = issue.path?.map((
|
|
192
|
+
const path = issue.path?.map((pathSegment) => String(pathSegment.key)).join(".") ?? "";
|
|
132
193
|
return { ok: false, error: `${path ? path + ": " : ""}${issue.message}` };
|
|
133
194
|
}
|