@corenel/harness 0.5.0 → 0.6.0

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 (37) hide show
  1. package/dist/api/backend.d.ts +24 -0
  2. package/dist/api/backend.d.ts.map +1 -1
  3. package/dist/api/backend.js +32 -8
  4. package/dist/api/backend.js.map +1 -1
  5. package/dist/rooms/findings.d.ts +134 -0
  6. package/dist/rooms/findings.d.ts.map +1 -0
  7. package/dist/rooms/findings.js +262 -0
  8. package/dist/rooms/findings.js.map +1 -0
  9. package/dist/sessions/fileSessionStore.d.ts +33 -1
  10. package/dist/sessions/fileSessionStore.d.ts.map +1 -1
  11. package/dist/sessions/fileSessionStore.js +139 -15
  12. package/dist/sessions/fileSessionStore.js.map +1 -1
  13. package/dist/sidecar/host.d.ts +115 -4
  14. package/dist/sidecar/host.d.ts.map +1 -1
  15. package/dist/sidecar/host.js +239 -3
  16. package/dist/sidecar/host.js.map +1 -1
  17. package/dist/sidecar/server.d.ts +136 -2
  18. package/dist/sidecar/server.d.ts.map +1 -1
  19. package/dist/sidecar/server.js +313 -2
  20. package/dist/sidecar/server.js.map +1 -1
  21. package/dist/tools/builtins.d.ts.map +1 -1
  22. package/dist/tools/builtins.js +8 -0
  23. package/dist/tools/builtins.js.map +1 -1
  24. package/dist/tools/files.d.ts.map +1 -1
  25. package/dist/tools/files.js +8 -0
  26. package/dist/tools/files.js.map +1 -1
  27. package/dist/tools/roomTools.d.ts +68 -5
  28. package/dist/tools/roomTools.d.ts.map +1 -1
  29. package/dist/tools/roomTools.js +210 -19
  30. package/dist/tools/roomTools.js.map +1 -1
  31. package/dist/tools/sessions.d.ts.map +1 -1
  32. package/dist/tools/sessions.js +1 -0
  33. package/dist/tools/sessions.js.map +1 -1
  34. package/dist/tools/web.d.ts.map +1 -1
  35. package/dist/tools/web.js +3 -0
  36. package/dist/tools/web.js.map +1 -1
  37. package/package.json +7 -2
@@ -1,4 +1,4 @@
1
- import type { AgentTool, AgentEvent, PermissionDecision, SidecarRoots, DaemonRunActivity, DaemonRegistration, SidecarProcs, SidecarSessions, RoomOpenRequest, RoomSummary } from '@corenel/protocol';
1
+ import type { AgentTool, AgentEvent, PermissionDecision, SidecarRoots, DaemonRunActivity, DaemonRegistration, AgentPause, HostSummary, SidecarProcs, SidecarSessions, RoomOpenRequest, RoomSummary, RoomFinding, RoomFindingEdit } from '@corenel/protocol';
2
2
  import { type SidecarChannel, type SidecarMsg, type SidecarInfo, type SidecarRunner, type SidecarHostSync, type SidecarAcp } from '@corenel/protocol';
3
3
  /** The payload of a `permission-ask` message, minus its `t` discriminant — derived
4
4
  * from the wire protocol so this stays in lockstep with it automatically. */
@@ -88,13 +88,83 @@ export interface SidecarRooms {
88
88
  messages: unknown[];
89
89
  turnCosts: number[];
90
90
  } | null>;
91
- answer(roomId: string, participantId: string, text: string): Promise<RoomSummary>;
91
+ /** `addressed` is who this human turn hands the floor to, resolved to an
92
+ * id. Omitted is an ordinary rotation turn. */
93
+ answer(roomId: string, participantId: string, text: string, addressed?: string): Promise<RoomSummary>;
92
94
  /** Drive a room whose floor is an AGENT's -- a dropped machine's park, or
93
95
  * anything a restart found mid-flight. See `room-resume` in sidecar.ts for
94
96
  * why nothing else can move such a room. */
95
97
  resume(roomId: string): Promise<RoomSummary>;
98
+ /** A human's words in a room whose floor is someone else's. Appends to the
99
+ * transcript and moves nothing -- see `room-interject` in sidecar.ts. */
100
+ interject(roomId: string, participantId: string, text: string): Promise<RoomSummary>;
101
+ /** End the room on a person's word, whatever the rotation is doing.
102
+ * Idempotent: an already-closed room answers with the close it already has. */
103
+ close(roomId: string, participantId: string, reason: string): Promise<RoomSummary>;
104
+ /** The room's living findings document. Empty for a room that has recorded
105
+ * nothing; throws only when the document exists and could not be read. */
106
+ findings(roomId: string): Promise<RoomFinding[]>;
107
+ /** Record, revise or withdraw ONE finding on a person's word, returning the
108
+ * document as it then stands. Throws for a refused edit (a closed room, a
109
+ * participant who is not a human member, an edit the rules reject) -- the
110
+ * caller renders the reason. */
111
+ editFinding(roomId: string, participantId: string, edit: RoomFindingEdit): Promise<RoomFinding[]>;
96
112
  subscribe(cb: (room: RoomSummary) => void): () => void;
97
113
  }
114
+ /** The daemon's pause overrides, behind the `agent-pause-*` messages.
115
+ *
116
+ * Declared HERE (harness-side), not imported from `@corenel/sidecar`, for the
117
+ * same dependency-direction reason as `SidecarAttendanceRegistry` /
118
+ * `SidecarRunHub` / `SidecarRooms` above: `packages/sidecar` depends on
119
+ * `packages/harness`, never the reverse. The daemon's concrete
120
+ * `AgentPauseStore` (packages/sidecar/src/runner/pauseStore.ts) has strictly
121
+ * more members and is structurally assignable.
122
+ *
123
+ * `set` returns the COMPLETE set afterwards so a client never reconciles a
124
+ * delta, and `refresh` is what re-derives the daemon's registrations, so a
125
+ * paused agent's row updates on every connected client rather than only on the
126
+ * one that asked. */
127
+ export interface SidecarPauses {
128
+ list(): AgentPause[];
129
+ set(owner: string, agent: string, paused: boolean): Promise<AgentPause[]>;
130
+ /** Re-derive and republish what this daemon will fire. */
131
+ refresh(): void;
132
+ }
133
+ /** The daemon's synced-host directories, behind the `hosts-*` messages.
134
+ *
135
+ * Declared HERE (harness-side) for the same dependency-direction reason as
136
+ * `SidecarPauses` above: `packages/sidecar` depends on `packages/harness`,
137
+ * never the reverse, so the harness knows the SHAPE and nothing about the
138
+ * manifest file or the filesystem underneath it.
139
+ *
140
+ * `forget` is the only destructive verb in this interface, and its refusal arm
141
+ * is load-bearing: a connected directory must be refused HERE, on the daemon,
142
+ * because a client can send any `dirName` it likes regardless of what its UI
143
+ * offered. `list` reports `connected` from the same live set, so the two can
144
+ * never disagree.
145
+ *
146
+ * `attach` is how that live set is kept: the harness calls it once per
147
+ * connection, as soon as this daemon has resolved which directory the
148
+ * connection belongs to, and calls the returned function when the channel
149
+ * closes. One implementation instance is shared by every per-connection
150
+ * `SidecarServer` (exactly like `rooms`/`runs`); two instances would each see
151
+ * half the connections and each would happily delete the other half's trees. */
152
+ export interface SidecarHosts {
153
+ /** Every manifest entry, with `self` left false -- the harness stamps it from
154
+ * the identity THIS connection authenticated with. */
155
+ list(): Promise<HostSummary[]>;
156
+ /** Remove one synced tree AND its manifest entry together. `ok: false` is a
157
+ * considered refusal (unknown name, a client connected as it, a removal that
158
+ * failed), never a thrown error. */
159
+ forget(dirName: string): Promise<{
160
+ ok: true;
161
+ } | {
162
+ ok: false;
163
+ reason: string;
164
+ }>;
165
+ /** Note that a connection is attached as `dirName`; returns its detach. */
166
+ attach(dirName: string): () => void;
167
+ }
98
168
  export interface SidecarServerOptions {
99
169
  channel: SidecarChannel;
100
170
  /** The pairing token a connecting host must present. */
@@ -163,6 +233,35 @@ export interface SidecarServerOptions {
163
233
  * `@corenel/sidecar` here -- the harness stays ignorant of the manifest and the
164
234
  * filesystem; only the shape from `@corenel/protocol` is known. */
165
235
  hostSync?: SidecarHostSync;
236
+ /** The daemon's pause overrides, behind `agent-pause-*`. Absent means this
237
+ * daemon holds none: it advertises no 'agent-pause' feature and answers every
238
+ * pause message with a refusal that says so, rather than accepting a pause it
239
+ * would never honour. */
240
+ pauses?: SidecarPauses;
241
+ /** The daemon's synced-host directories, behind `hosts-*`. Absent means this
242
+ * daemon exposes no host administration: it advertises no 'hosts-admin'
243
+ * feature and answers every hosts message with a refusal that says so,
244
+ * rather than silently reporting an empty estate. */
245
+ hosts?: SidecarHosts;
246
+ /**
247
+ * The synced directory name this daemon routes THIS client's host sync into,
248
+ * echoed back in the `welcome` so the client can tell which registrations are
249
+ * its own (see `welcome.dirName` in sidecar.ts).
250
+ *
251
+ * Awaited before the welcome is sent -- unlike `onClient`, which is
252
+ * deliberately fire-and-forget and must never delay the handshake. The wait is
253
+ * bounded, and every failure mode (a throw, a rejection, a null, a timeout)
254
+ * ends the same way: the welcome goes out with no `dirName`, which an older
255
+ * client already handles because an older daemon never sent one.
256
+ *
257
+ * It resolves the name from the AUTHENTICATED `clientId` this connection
258
+ * presented. It must never accept a name the client asked for -- that would
259
+ * reintroduce the client-chosen destination `host-sync-put` exists to refuse.
260
+ */
261
+ clientDirName?: (identity: {
262
+ clientId: string;
263
+ name: string;
264
+ }) => Promise<string | null>;
166
265
  }
167
266
  export declare class SidecarServer {
168
267
  private readonly ch;
@@ -183,10 +282,20 @@ export declare class SidecarServer {
183
282
  * connection carries no identity to attribute files to" (an older client).
184
283
  * Either way, host-sync must be rejected; see `requireHostSyncIdentity`. */
185
284
  private clientId?;
285
+ /** The friendly label from the same `hello`, kept beside `clientId` so the
286
+ * dirName resolver is handed the identity the manifest keys on. */
287
+ private clientName?;
186
288
  /** Detach function for this connection's attendance attachment, set on a
187
289
  * successful `hello` when `opts.attendance` is wired. Called once, on channel
188
290
  * close, so a dead connection stops receiving/counting toward asks. */
189
291
  private attendedDetach?;
292
+ /** The synced directory THIS connection belongs to, once resolved -- the same
293
+ * value echoed in `welcome.dirName`. Retained so `hosts-list` can stamp
294
+ * `self` and `hosts-forget` can refuse the caller's own tree from the
295
+ * daemon's own knowledge rather than from anything the client claimed. */
296
+ private dirName?;
297
+ /** Detach for this connection's entry in the shared hosts live set. */
298
+ private hostsDetach?;
190
299
  /** Detach callbacks for runs this connection is watching, so a dropped socket
191
300
  * stops the hub fanning out into a dead channel. */
192
301
  private readonly runDetachers;
@@ -200,6 +309,10 @@ export declare class SidecarServer {
200
309
  constructor(opts: SidecarServerOptions);
201
310
  private onMessage;
202
311
  private onHello;
312
+ /** Resolve the connection's synced directory name, or null for every failure
313
+ * mode there is (throw, rejection, non-string, timeout). */
314
+ private resolveDirName;
315
+ private sendWelcome;
203
316
  /** Proxy a host HTTP request server-side. Restricted to LOOPBACK targets — the
204
317
  * sidecar must never be usable as an SSRF pivot to internal/metadata hosts. The
205
318
  * response streams back (http-res -> http-chunk* -> http-end). */
@@ -280,11 +393,32 @@ export declare class SidecarServer {
280
393
  private requireHostSyncIdentity;
281
394
  private onRootsBrowse;
282
395
  private onRootsAdd;
396
+ /** Every synced host directory, with `self` stamped from THIS connection's
397
+ * own resolved directory -- never from anything the client sent. */
398
+ private onHostsList;
399
+ private stampSelf;
400
+ /**
401
+ * Delete one synced tree and its manifest entry.
402
+ *
403
+ * THE REFUSALS ARE THE FEATURE. A client's UI is not a guard -- this message
404
+ * carries a `dirName` the client chose, so both rules that make the operation
405
+ * safe are enforced here: the caller's OWN directory is refused outright, and
406
+ * the port refuses any directory a client is attached as. The first is
407
+ * checked here rather than left to the second because it is answerable from
408
+ * this connection's own identity and yields the more useful sentence.
409
+ */
410
+ private onHostsForget;
411
+ private onAgentPauseList;
412
+ private onAgentPauseSet;
283
413
  private onRoomOpen;
284
414
  private onRoomList;
285
415
  private onRoomRead;
286
416
  private onRoomResume;
287
417
  private onRoomAnswer;
418
+ private onRoomInterject;
419
+ private onRoomClose;
420
+ private onRoomFindings;
421
+ private onRoomFindingEdit;
288
422
  private onHostSyncPut;
289
423
  private onHostSyncDelete;
290
424
  /** A ToolContext whose capabilities proxy back to the host (persist-nothing).
@@ -1 +1 @@
1
- {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../sidecar/server.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EACV,SAAS,EAEmC,UAAU,EAAE,kBAAkB,EAAE,YAAY,EAAE,iBAAiB,EAAE,kBAAkB,EAC/H,YAAY,EAAE,eAAe,EAAe,eAAe,EAAE,WAAW,EACzE,MAAM,mBAAmB,CAAC;AAE3B,OAAO,EAEL,KAAK,cAAc,EAAE,KAAK,UAAU,EAAoC,KAAK,WAAW,EACxF,KAAK,aAAa,EAAE,KAAK,eAAe,EAAE,KAAK,UAAU,EAC1D,MAAM,mBAAmB,CAAC;AAE3B;6EAC6E;AAC7E,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,CAAC,UAAU,EAAE;IAAE,CAAC,EAAE,gBAAgB,CAAA;CAAE,CAAC,EAAE,GAAG,CAAC,CAAC;AAEpF;;;;;;qFAMqF;AACrF,MAAM,WAAW,yBAAyB;IACxC,8EAA8E;IAC9E,MAAM,CAAC,MAAM,EAAE;QACb,IAAI,CAAC,GAAG,EAAE,aAAa,GAAG,IAAI,CAAC;QAC/B,QAAQ,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,EAAE,EAAE,EAAE,QAAQ,GAAG,SAAS,GAAG,YAAY,GAAG,IAAI,CAAC;QAClG;;2EAEmE;QACnE,WAAW,CAAC,CAAC,QAAQ,EAAE,iBAAiB,GAAG,IAAI,CAAC;QAChD,aAAa,CAAC,CAAC,IAAI,EAAE,kBAAkB,EAAE,GAAG,IAAI,CAAC;KAClD,GAAG,MAAM,IAAI,CAAC;IACf;sCACkC;IAClC,MAAM,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,kBAAkB,GAAG,IAAI,CAAC;CACxD;AAED;;;;;kFAKkF;AAClF,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,GAAG,IAAI,CAAC;IAChD,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,UAAU,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC1E,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3C,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,EAAE;QAC5B,KAAK,CAAC,CAAC,EAAE,UAAU,GAAG,IAAI,CAAC;QAC3B,IAAI,CAAC,MAAM,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,UAAU,EAAE,MAAM,CAAA;SAAE,GAAG,IAAI,CAAC;QACzD,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;KAC9B,GAAG;QAAE,MAAM,EAAE;YAAE,MAAM,EAAE,UAAU,EAAE,CAAC;YAAC,OAAO,EAAE,MAAM,CAAC;YAAC,IAAI,CAAC,EAAE;gBAAE,IAAI,EAAE,MAAM,CAAC;gBAAC,UAAU,EAAE,MAAM,CAAA;aAAE,CAAC;YAAC,KAAK,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QAAC,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;CACnJ;AAED;;;;;;;;;;;;qBAYqB;AACrB,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC;QAAE,EAAE,EAAE,IAAI,CAAC;QAAC,IAAI,EAAE,WAAW,CAAA;KAAE,GAAG;QAAE,EAAE,EAAE,KAAK,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACzG,IAAI,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC;IAC/B,IAAI,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,IAAI,EAAE,WAAW,CAAC;QAAC,QAAQ,EAAE,OAAO,EAAE,CAAC;QAAC,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IACtG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAClF;;gDAE4C;IAC5C,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC7C,SAAS,CAAC,EAAE,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CACxD;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,cAAc,CAAC;IACxB,wDAAwD;IACxD,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB;gFAC4E;IAC5E,IAAI,CAAC,EAAE,WAAW,CAAC;IACnB;4FACwF;IACxF,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B;iFAC6E;IAC7E,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;;+CAG2C;IAC3C,UAAU,CAAC,EAAE,yBAAyB,CAAC;IACvC;;kEAE8D;IAC9D,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB;;0CAEsC;IACtC,IAAI,CAAC,EAAE,aAAa,CAAC;IACrB;;kDAE8C;IAC9C,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;;4EAGwE;IACxE,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;;;2EAIuE;IACvE,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB;;wEAEoE;IACpE,QAAQ,CAAC,EAAE,eAAe,CAAC;IAC3B;;;;;;;;;;4BAUwB;IACxB,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAClE;;;;uEAImE;IACnE,QAAQ,CAAC,EAAE,eAAe,CAAC;CAC5B;AAqCD,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAiB;IACpC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAuB;IAC5C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,QAAQ,CAAC,WAAW,CAA8B;IAC1D;;0DAEsD;IACtD,OAAO,CAAC,YAAY,CAAc;IAClC;;;;;;gFAM4E;IAC5E,OAAO,CAAC,QAAQ,CAAC,CAAS;IAC1B;;2EAEuE;IACvE,OAAO,CAAC,cAAc,CAAC,CAAa;IACpC;wDACoD;IACpD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAyB;IAEtD;;;;iEAI6D;IAC7D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAqB;IACnD,OAAO,CAAC,SAAS,CAA2B;gBAEhC,IAAI,EAAE,oBAAoB;IAkDtC,OAAO,CAAC,SAAS;IAkKjB,OAAO,CAAC,OAAO;IAyFf;;sEAEkE;YACpD,SAAS;IA0BvB;;;;gEAI4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqC;IAElE,OAAO,CAAC,eAAe;YAMT,SAAS;IAkGvB,OAAO,CAAC,YAAY;IASpB;;wBAEoB;IACpB,OAAO,CAAC,YAAY;IAMpB;;mDAE+C;IAC/C,OAAO,CAAC,gBAAgB;YAoBV,WAAW;IA6BzB;4EACwE;IACxE,OAAO,CAAC,YAAY;IAoBpB,mEAAmE;IACnE,OAAO,CAAC,YAAY;YASN,QAAQ;IAqCtB;;;;;;;;;uCASmC;YACrB,UAAU;YAwBV,YAAY;YAcZ,WAAW;IAczB;;;;;;;OAOG;IACH,OAAO,CAAC,WAAW;IA+CnB;;;;;;;;;OASG;YACW,YAAY;IAa1B;+EAC2E;YAC7D,eAAe;IAW7B;;;;iBAIa;YACC,YAAY;YAcZ,KAAK;IA+DnB;;;;;4EAKwE;IACxE,OAAO,CAAC,uBAAuB;YAQjB,aAAa;YAYb,UAAU;YAeV,UAAU;YAiBV,UAAU;YAkBV,UAAU;YAqBV,YAAY;YAeZ,YAAY;YAeZ,aAAa;YAkBb,gBAAgB;IAkB9B;;;;oEAIgE;IAChE,OAAO,CAAC,WAAW;IAanB,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAKrB;IAEF,OAAO,CAAC,QAAQ,CAAC,EAAE,CAKjB;IAEF,OAAO,CAAC,QAAQ,CAAC,MAAM,CAErB;IAEF,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAOvB;IAEF;mFAC+E;IAC/E,OAAO,CAAC,QAAQ,CAAC,KAAK,CAcpB;IAEF,OAAO,CAAC,QAAQ,CAAC,OAAO,CAC2D;IAEnF,OAAO,CAAC,QAAQ,CAAC,GAAG,CACuE;CAC5F"}
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../../sidecar/server.ts"],"names":[],"mappings":"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"}
@@ -1,4 +1,9 @@
1
1
  import { SIDECAR_PROTOCOL, errMessage, isLoopbackHost, } from '@corenel/protocol';
2
+ /** How long the welcome waits on `clientDirName` before going out without it. A
3
+ * manifest write is a few milliseconds; anything near this is a wedged disk, and
4
+ * a client left with no handshake at all is worse than one that has to fall back
5
+ * to deducing its own directory. */
6
+ const DIR_NAME_TIMEOUT_MS = 3000;
2
7
  /** True for anything with a callable `.then` -- covers native Promises and any
3
8
  * other thenable a caller's `onClient` might return, without assuming it's a
4
9
  * real Promise instance (the declared type is `() => void`; an async function
@@ -91,6 +96,14 @@ export class SidecarServer {
91
96
  writable: true,
92
97
  value: void 0
93
98
  });
99
+ /** The friendly label from the same `hello`, kept beside `clientId` so the
100
+ * dirName resolver is handed the identity the manifest keys on. */
101
+ Object.defineProperty(this, "clientName", {
102
+ enumerable: true,
103
+ configurable: true,
104
+ writable: true,
105
+ value: void 0
106
+ });
94
107
  /** Detach function for this connection's attendance attachment, set on a
95
108
  * successful `hello` when `opts.attendance` is wired. Called once, on channel
96
109
  * close, so a dead connection stops receiving/counting toward asks. */
@@ -100,6 +113,23 @@ export class SidecarServer {
100
113
  writable: true,
101
114
  value: void 0
102
115
  });
116
+ /** The synced directory THIS connection belongs to, once resolved -- the same
117
+ * value echoed in `welcome.dirName`. Retained so `hosts-list` can stamp
118
+ * `self` and `hosts-forget` can refuse the caller's own tree from the
119
+ * daemon's own knowledge rather than from anything the client claimed. */
120
+ Object.defineProperty(this, "dirName", {
121
+ enumerable: true,
122
+ configurable: true,
123
+ writable: true,
124
+ value: void 0
125
+ });
126
+ /** Detach for this connection's entry in the shared hosts live set. */
127
+ Object.defineProperty(this, "hostsDetach", {
128
+ enumerable: true,
129
+ configurable: true,
130
+ writable: true,
131
+ value: void 0
132
+ });
103
133
  /** Detach callbacks for runs this connection is watching, so a dropped socket
104
134
  * stops the hub fanning out into a dead channel. */
105
135
  Object.defineProperty(this, "runDetachers", {
@@ -247,6 +277,10 @@ export class SidecarServer {
247
277
  this.ch.onClose?.(() => {
248
278
  this.attendedDetach?.();
249
279
  this.attendedDetach = undefined;
280
+ // Leave the live set, or this directory reads as connected for ever and
281
+ // could never be reclaimed -- the refusal would outlive the connection.
282
+ this.hostsDetach?.();
283
+ this.hostsDetach = undefined;
250
284
  // A dropped connection must stop receiving run events, or the hub keeps
251
285
  // fanning out into a dead channel for the rest of the run.
252
286
  for (const detach of this.runDetachers)
@@ -452,6 +486,40 @@ export class SidecarServer {
452
486
  case 'host-sync-delete':
453
487
  void this.onHostSyncDelete(m);
454
488
  return;
489
+ case 'agent-pause-list':
490
+ // `ok: false`, not an empty list: telling an unauthenticated caller
491
+ // "this daemon is holding no pauses" is a claim it has not earned, the
492
+ // same reasoning as room-list below.
493
+ if (!this.authed) {
494
+ this.ch.send({ t: 'agent-pause-list-result', id: m.id, ok: false, error: 'not authenticated' });
495
+ return;
496
+ }
497
+ this.onAgentPauseList(m);
498
+ return;
499
+ case 'agent-pause-set':
500
+ if (!this.authed) {
501
+ this.ch.send({ t: 'agent-pause-set-result', id: m.id, ok: false, error: 'not authenticated' });
502
+ return;
503
+ }
504
+ void this.onAgentPauseSet(m);
505
+ return;
506
+ case 'hosts-list':
507
+ // `ok: false`, not an empty estate: telling an unauthenticated caller
508
+ // "this daemon holds no synced hosts" is a claim it has not earned --
509
+ // the same reasoning as agent-pause-list above.
510
+ if (!this.authed) {
511
+ this.ch.send({ t: 'hosts-list-result', id: m.id, ok: false, error: 'not authenticated' });
512
+ return;
513
+ }
514
+ void this.onHostsList(m);
515
+ return;
516
+ case 'hosts-forget':
517
+ if (!this.authed) {
518
+ this.ch.send({ t: 'hosts-forget-result', id: m.id, ok: false, error: 'not authenticated' });
519
+ return;
520
+ }
521
+ void this.onHostsForget(m);
522
+ return;
455
523
  case 'room-open':
456
524
  if (!this.authed) {
457
525
  this.ch.send({ t: 'room-open-result', id: m.id, ok: false, problems: ['not authenticated'] });
@@ -492,6 +560,34 @@ export class SidecarServer {
492
560
  }
493
561
  void this.onRoomResume(m);
494
562
  return;
563
+ case 'room-interject':
564
+ if (!this.authed) {
565
+ this.ch.send({ t: 'room-interject-result', id: m.id, ok: false, error: 'not authenticated' });
566
+ return;
567
+ }
568
+ void this.onRoomInterject(m);
569
+ return;
570
+ case 'room-findings':
571
+ if (!this.authed) {
572
+ this.ch.send({ t: 'room-findings-result', id: m.id, ok: false, error: 'not authenticated' });
573
+ return;
574
+ }
575
+ void this.onRoomFindings(m);
576
+ return;
577
+ case 'room-finding-edit':
578
+ if (!this.authed) {
579
+ this.ch.send({ t: 'room-finding-edit-result', id: m.id, ok: false, error: 'not authenticated' });
580
+ return;
581
+ }
582
+ void this.onRoomFindingEdit(m);
583
+ return;
584
+ case 'room-close':
585
+ if (!this.authed) {
586
+ this.ch.send({ t: 'room-close-result', id: m.id, ok: false, error: 'not authenticated' });
587
+ return;
588
+ }
589
+ void this.onRoomClose(m);
590
+ return;
495
591
  default:
496
592
  // welcome / denied / invoke-result / host-call / event / run-accepted /
497
593
  // run-done / permission-ask / permission-resolved are host-side.
@@ -523,6 +619,7 @@ export class SidecarServer {
523
619
  const name = typeof m.clientName === 'string' && m.clientName
524
620
  ? m.clientName.slice(0, MAX_IDENTITY_FIELD_LEN)
525
621
  : clientId;
622
+ this.clientName = name;
526
623
  // Best-effort, fire-and-forget: a throwing/rejecting `onClient` must never
527
624
  // deny/break an otherwise valid, authenticated handshake -- identity
528
625
  // recording is informational, not part of the auth decision. Covers BOTH
@@ -552,6 +649,52 @@ export class SidecarServer {
552
649
  });
553
650
  }
554
651
  this.opts.log?.(`paired — ${this.tools.size} ${this.tools.size === 1 ? 'tool' : 'tools'} advertised (host dir: ${this.hostStateDir})`);
652
+ /* THE ONE THING THAT WAITS. The welcome carries the synced directory name
653
+ * this daemon resolved for this connection, and the client has no other way
654
+ * to learn it -- it could previously only deduce it, and got null whenever
655
+ * no workspace was open or two hosts held the same workspace id, leaving a
656
+ * management surface unable to say which registrations were its own.
657
+ *
658
+ * Bounded, and every failure ends the same way as an older daemon: no
659
+ * `dirName` on the welcome. A handshake that never arrives would be a far
660
+ * worse trade than a client that falls back to deducing. */
661
+ const identity = this.clientId === undefined
662
+ ? undefined
663
+ : { clientId: this.clientId, name: this.clientName ?? this.clientId };
664
+ if (this.opts.clientDirName && identity) {
665
+ void this.resolveDirName(this.opts.clientDirName, identity).then((dirName) => {
666
+ this.sendWelcome(dirName);
667
+ });
668
+ return;
669
+ }
670
+ this.sendWelcome(null);
671
+ }
672
+ /** Resolve the connection's synced directory name, or null for every failure
673
+ * mode there is (throw, rejection, non-string, timeout). */
674
+ async resolveDirName(resolve, identity) {
675
+ try {
676
+ const timed = new Promise((res) => setTimeout(() => res(null), DIR_NAME_TIMEOUT_MS));
677
+ const got = await Promise.race([Promise.resolve(resolve(identity)), timed]);
678
+ return typeof got === 'string' && got ? got : null;
679
+ }
680
+ catch (err) {
681
+ this.opts.log?.(`clientDirName failed: ${errMessage(err)}`);
682
+ return null;
683
+ }
684
+ }
685
+ sendWelcome(dirName) {
686
+ /* Remembered BEFORE the welcome goes out, because `self` and the
687
+ * forget-your-own-tree refusal are both answered from it. A daemon too old
688
+ * to resolve one, or a client with no identity, leaves it undefined -- and
689
+ * then every entry reads `self: false` and nothing is refused on that
690
+ * ground, which is correct: this connection genuinely has no directory. */
691
+ if (dirName) {
692
+ this.dirName = dirName;
693
+ // Only a resolved directory can be attached: an unidentified connection
694
+ // is not "connected as" anything, and marking it would pin a tree it
695
+ // has no claim to.
696
+ this.hostsDetach = this.opts.hosts?.attach(dirName);
697
+ }
555
698
  const tools = [...this.tools.values()].map(({ run: _run, ...decl }) => decl);
556
699
  // Advertise the loopback HTTP proxy so the host can route local-LLM calls
557
700
  // through us (server-side fetch, no browser CORS) instead of failing, and
@@ -583,8 +726,21 @@ export class SidecarServer {
583
726
  // operator declared somewhere a checkout may be written, so a client only
584
727
  // offers "start from a repository" where it would actually work.
585
728
  ...(this.opts.sessions?.canClone ? ['session-clone'] : []),
729
+ // Whether this daemon HOLDS pauses. Advertised only with a real store
730
+ // behind it, so a client offers "Pause" only where pausing would actually
731
+ // stop the agent -- the alternative is a button that reports success on a
732
+ // daemon that keeps firing.
733
+ ...(this.opts.pauses ? ['agent-pause'] : []),
734
+ // Whether this daemon can report and reclaim its synced host directories.
735
+ // Advertised only with a real manifest behind it, so a client never
736
+ // offers a destructive control to a daemon that would refuse it.
737
+ ...(this.opts.hosts ? ['hosts-admin'] : []),
586
738
  ];
587
- this.ch.send({ t: 'welcome', tools, features, ...(this.opts.info ? { info: this.opts.info } : {}) });
739
+ this.ch.send({
740
+ t: 'welcome', tools, features,
741
+ ...(this.opts.info ? { info: this.opts.info } : {}),
742
+ ...(dirName ? { dirName } : {}),
743
+ });
588
744
  }
589
745
  /** Proxy a host HTTP request server-side. Restricted to LOOPBACK targets — the
590
746
  * sidecar must never be usable as an SSRF pivot to internal/metadata hosts. The
@@ -1159,6 +1315,103 @@ export class SidecarServer {
1159
1315
  this.ch.send({ t: 'roots-add-result', id: m.id, ok: false, error: errMessage(e) });
1160
1316
  }
1161
1317
  }
1318
+ /** Every synced host directory, with `self` stamped from THIS connection's
1319
+ * own resolved directory -- never from anything the client sent. */
1320
+ async onHostsList(m) {
1321
+ if (!this.opts.hosts) {
1322
+ this.ch.send({ t: 'hosts-list-result', id: m.id, ok: false, error: 'this daemon does not administer synced hosts' });
1323
+ return;
1324
+ }
1325
+ try {
1326
+ this.ch.send({ t: 'hosts-list-result', id: m.id, ok: true, hosts: this.stampSelf(await this.opts.hosts.list()) });
1327
+ }
1328
+ catch (err) {
1329
+ this.ch.send({ t: 'hosts-list-result', id: m.id, ok: false, error: errMessage(err) });
1330
+ }
1331
+ }
1332
+ stampSelf(hosts) {
1333
+ return hosts.map((h) => ({ ...h, self: this.dirName !== undefined && h.dirName === this.dirName }));
1334
+ }
1335
+ /**
1336
+ * Delete one synced tree and its manifest entry.
1337
+ *
1338
+ * THE REFUSALS ARE THE FEATURE. A client's UI is not a guard -- this message
1339
+ * carries a `dirName` the client chose, so both rules that make the operation
1340
+ * safe are enforced here: the caller's OWN directory is refused outright, and
1341
+ * the port refuses any directory a client is attached as. The first is
1342
+ * checked here rather than left to the second because it is answerable from
1343
+ * this connection's own identity and yields the more useful sentence.
1344
+ */
1345
+ async onHostsForget(m) {
1346
+ if (!this.opts.hosts) {
1347
+ this.ch.send({ t: 'hosts-forget-result', id: m.id, ok: false, error: 'this daemon does not administer synced hosts' });
1348
+ return;
1349
+ }
1350
+ if (typeof m.dirName !== 'string' || !m.dirName) {
1351
+ this.ch.send({ t: 'hosts-forget-result', id: m.id, ok: false, error: 'dirName must be a non-empty string' });
1352
+ return;
1353
+ }
1354
+ if (this.dirName !== undefined && m.dirName === this.dirName) {
1355
+ this.ch.send({
1356
+ t: 'hosts-forget-result', id: m.id, ok: false,
1357
+ error: 'That is this connection’s own synced directory. Disconnect this client first, then remove it from another one.',
1358
+ });
1359
+ return;
1360
+ }
1361
+ try {
1362
+ const outcome = await this.opts.hosts.forget(m.dirName);
1363
+ if (!outcome.ok) {
1364
+ this.ch.send({ t: 'hosts-forget-result', id: m.id, ok: false, error: outcome.reason });
1365
+ return;
1366
+ }
1367
+ this.ch.send({ t: 'hosts-forget-result', id: m.id, ok: true, hosts: this.stampSelf(await this.opts.hosts.list()) });
1368
+ }
1369
+ catch (err) {
1370
+ this.ch.send({ t: 'hosts-forget-result', id: m.id, ok: false, error: errMessage(err) });
1371
+ }
1372
+ }
1373
+ onAgentPauseList(m) {
1374
+ if (!this.opts.pauses) {
1375
+ // An honest empty answer for a daemon without the capability: it is
1376
+ // holding no pauses, which is true. The feature gate is what tells a
1377
+ // client whether it may OFFER the control.
1378
+ this.ch.send({ t: 'agent-pause-list-result', id: m.id, ok: true, pauses: [] });
1379
+ return;
1380
+ }
1381
+ try {
1382
+ this.ch.send({ t: 'agent-pause-list-result', id: m.id, ok: true, pauses: this.opts.pauses.list() });
1383
+ }
1384
+ catch (err) {
1385
+ this.ch.send({ t: 'agent-pause-list-result', id: m.id, ok: false, error: errMessage(err) });
1386
+ }
1387
+ }
1388
+ async onAgentPauseSet(m) {
1389
+ if (!this.opts.pauses) {
1390
+ // NOT an empty success. Accepting a pause this daemon cannot hold would
1391
+ // leave the operator believing an agent had been stopped while it kept
1392
+ // firing on the hour -- the exact failure the feature exists to end.
1393
+ this.ch.send({ t: 'agent-pause-set-result', id: m.id, ok: false, error: 'this daemon does not hold agent pauses' });
1394
+ return;
1395
+ }
1396
+ // Runtime-validate: the message arrived via `JSON.parse(...) as SidecarMsg`,
1397
+ // so the declared types guarantee nothing. Both fields become part of a
1398
+ // persisted key.
1399
+ if (typeof m.owner !== 'string' || typeof m.agent !== 'string' || !m.agent) {
1400
+ this.ch.send({ t: 'agent-pause-set-result', id: m.id, ok: false, error: 'owner must be a string and agent a non-empty string' });
1401
+ return;
1402
+ }
1403
+ try {
1404
+ const pauses = await this.opts.pauses.set(m.owner, m.agent, m.paused === true);
1405
+ // Republish before replying: the caller's own row and every other
1406
+ // connected client's must change together, or a second browser keeps
1407
+ // offering "Pause" on an agent already paused.
1408
+ this.opts.pauses.refresh();
1409
+ this.ch.send({ t: 'agent-pause-set-result', id: m.id, ok: true, pauses });
1410
+ }
1411
+ catch (err) {
1412
+ this.ch.send({ t: 'agent-pause-set-result', id: m.id, ok: false, error: errMessage(err) });
1413
+ }
1414
+ }
1162
1415
  async onRoomOpen(m) {
1163
1416
  if (!this.opts.rooms) {
1164
1417
  this.ch.send({ t: 'room-open-result', id: m.id, ok: false, problems: ['this sidecar does not host rooms'] });
@@ -1236,7 +1489,7 @@ export class SidecarServer {
1236
1489
  return;
1237
1490
  }
1238
1491
  try {
1239
- const room = await this.opts.rooms.answer(m.roomId, m.participantId, m.text);
1492
+ const room = await this.opts.rooms.answer(m.roomId, m.participantId, m.text, m.addressed);
1240
1493
  this.ch.send({ t: 'room-answer-result', id: m.id, ok: true, room });
1241
1494
  }
1242
1495
  catch (err) {
@@ -1245,6 +1498,64 @@ export class SidecarServer {
1245
1498
  this.ch.send({ t: 'room-answer-result', id: m.id, ok: false, error: errMessage(err) });
1246
1499
  }
1247
1500
  }
1501
+ async onRoomInterject(m) {
1502
+ if (!this.opts.rooms) {
1503
+ this.ch.send({ t: 'room-interject-result', id: m.id, ok: false, error: 'this sidecar does not host rooms' });
1504
+ return;
1505
+ }
1506
+ try {
1507
+ const room = await this.opts.rooms.interject(m.roomId, m.participantId, m.text);
1508
+ this.ch.send({ t: 'room-interject-result', id: m.id, ok: true, room });
1509
+ }
1510
+ catch (err) {
1511
+ // A room failure must never kill the connection; the client gets a
1512
+ // refusal it can render instead of a socket that went quiet.
1513
+ this.ch.send({ t: 'room-interject-result', id: m.id, ok: false, error: errMessage(err) });
1514
+ }
1515
+ }
1516
+ async onRoomClose(m) {
1517
+ if (!this.opts.rooms) {
1518
+ this.ch.send({ t: 'room-close-result', id: m.id, ok: false, error: 'this sidecar does not host rooms' });
1519
+ return;
1520
+ }
1521
+ try {
1522
+ const room = await this.opts.rooms.close(m.roomId, m.participantId, m.reason);
1523
+ this.ch.send({ t: 'room-close-result', id: m.id, ok: true, room });
1524
+ }
1525
+ catch (err) {
1526
+ this.ch.send({ t: 'room-close-result', id: m.id, ok: false, error: errMessage(err) });
1527
+ }
1528
+ }
1529
+ async onRoomFindings(m) {
1530
+ if (!this.opts.rooms) {
1531
+ // A daemon without the capability hosts no rooms, so it holds no
1532
+ // findings -- an honest empty document, the same answer `room-list`
1533
+ // gives. The catch below is the different thing: a real read that failed.
1534
+ this.ch.send({ t: 'room-findings-result', id: m.id, ok: true, findings: [] });
1535
+ return;
1536
+ }
1537
+ try {
1538
+ this.ch.send({ t: 'room-findings-result', id: m.id, ok: true, findings: await this.opts.rooms.findings(m.roomId) });
1539
+ }
1540
+ catch (err) {
1541
+ this.ch.send({ t: 'room-findings-result', id: m.id, ok: false, error: errMessage(err) });
1542
+ }
1543
+ }
1544
+ async onRoomFindingEdit(m) {
1545
+ if (!this.opts.rooms) {
1546
+ this.ch.send({ t: 'room-finding-edit-result', id: m.id, ok: false, error: 'this sidecar does not host rooms' });
1547
+ return;
1548
+ }
1549
+ try {
1550
+ const findings = await this.opts.rooms.editFinding(m.roomId, m.participantId, m.edit);
1551
+ this.ch.send({ t: 'room-finding-edit-result', id: m.id, ok: true, findings });
1552
+ }
1553
+ catch (err) {
1554
+ // A refused edit must never kill the connection; the person gets the
1555
+ // reason their edit did not land.
1556
+ this.ch.send({ t: 'room-finding-edit-result', id: m.id, ok: false, error: errMessage(err) });
1557
+ }
1558
+ }
1248
1559
  async onHostSyncPut(m) {
1249
1560
  const identity = this.requireHostSyncIdentity();
1250
1561
  if ('error' in identity) {