@roamcode.ai/server 1.0.22 → 1.1.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.
package/dist/start.d.ts CHANGED
@@ -18,7 +18,26 @@ interface AttachSpawnOptions {
18
18
  dataDir: string;
19
19
  }
20
20
 
21
- type ProviderId = "claude" | "codex";
21
+ declare const ADAPTER_CONTRACT_VERSION: 1;
22
+ declare const adapterCapabilityNames: readonly ["probe", "launch", "resume", "state", "identity", "metadata", "usage", "login", "attachments", "cleanup"];
23
+ type AdapterCapabilityName = (typeof adapterCapabilityNames)[number];
24
+ type AdapterStateAuthority = "native-events" | "runtime-signals" | "pane-heuristics";
25
+ interface AdapterManifestV1 {
26
+ schemaVersion: typeof ADAPTER_CONTRACT_VERSION;
27
+ id: string;
28
+ version: string;
29
+ displayName: string;
30
+ platforms: Array<"darwin" | "linux">;
31
+ resumeIdentity: "optional" | "required" | "unsupported";
32
+ capabilities: Record<AdapterCapabilityName, boolean>;
33
+ stateAuthority: AdapterStateAuthority[];
34
+ /** JSON Schema for the adapter-owned launch options. */
35
+ optionSchema: Record<string, unknown>;
36
+ }
37
+
38
+ /** Public adapter identity. Built-ins are ordinary ids under the same contract; third parties are not
39
+ * forced through a source-code union update. Runtime validation is owned by ProviderRegistry. */
40
+ type ProviderId = string;
22
41
  interface ProviderAvailability {
23
42
  terminalAvailable: boolean;
24
43
  metadataAvailable: boolean;
@@ -45,7 +64,22 @@ type CodexSessionOptions = {
45
64
  dangerouslyBypassApprovalsAndSandbox?: boolean;
46
65
  addDirs?: string[];
47
66
  };
48
- type ProviderSessionOptions = ClaudeSessionOptions | CodexSessionOptions;
67
+ interface ProviderSessionOptions {
68
+ provider: ProviderId;
69
+ model?: string;
70
+ effort?: string;
71
+ reasoningEffort?: string;
72
+ permissionMode?: ClaudeSessionOptions["permissionMode"];
73
+ dangerouslySkip?: boolean;
74
+ sandbox?: CodexSessionOptions["sandbox"];
75
+ approvalPolicy?: CodexSessionOptions["approvalPolicy"];
76
+ profile?: string;
77
+ webSearch?: boolean;
78
+ dangerouslyBypassApprovalsAndSandbox?: boolean;
79
+ addDirs?: string[];
80
+ legacyArgs?: string[];
81
+ [key: string]: unknown;
82
+ }
49
83
  type LaunchIntent = "fresh" | "resume";
50
84
  interface ProcessSpec {
51
85
  executable: string;
@@ -89,9 +123,11 @@ interface ProviderRuntimeMetadata {
89
123
  effort?: string;
90
124
  }
91
125
  interface AgentProvider {
126
+ /** Exact public, versioned adapter contract. Optional only for legacy in-process test doubles. */
127
+ readonly manifest?: Readonly<AdapterManifestV1>;
92
128
  readonly id: ProviderId;
93
129
  readonly displayName: string;
94
- readonly resumeIdentity: "optional" | "required";
130
+ readonly resumeIdentity: "optional" | "required" | "unsupported";
95
131
  probe(): Promise<ProviderAvailability>;
96
132
  buildProcess(context: ProviderProcessContext): Promise<ProcessSpec>;
97
133
  /** Stateful parsers must be created per spawned process; providers are registry singletons. */
@@ -116,6 +152,12 @@ interface ClaudeVersionProbe {
116
152
 
117
153
  interface AuthGateOptions {
118
154
  token?: string;
155
+ /**
156
+ * Optional verifier for independently revocable credentials (for example per-device tokens). It
157
+ * receives the presented plaintext once; implementations should persist only a digest. A verifier
158
+ * failure is treated as an invalid credential and can never take the server down.
159
+ */
160
+ verifyCredential?: (token: string) => boolean;
119
161
  /** Consecutive failures from one client before it is locked out. Default 10. */
120
162
  maxFailures?: number;
121
163
  /** Lockout duration in ms. Default 60000. */
@@ -159,6 +201,7 @@ declare class AuthGate {
159
201
  private readonly lockoutMs;
160
202
  private readonly graceMs;
161
203
  private readonly now;
204
+ private readonly verifyCredential?;
162
205
  private readonly clients;
163
206
  constructor(opts: AuthGateOptions);
164
207
  check(presentedToken: string | undefined, clientKey: string): AuthCheckResult;
@@ -171,6 +214,10 @@ declare class AuthGate {
171
214
  * slate is correct (and avoids leaving a client locked out against a token that no longer exists).
172
215
  */
173
216
  rotateToken(newToken: string): void;
217
+ /** True only for the current host recovery credential (never a device key or grace token). */
218
+ isCurrentHostToken(presentedToken: string | undefined): boolean;
219
+ /** Security reset: replace the host credential immediately, with no previous-token grace window. */
220
+ resetToken(newToken: string): void;
174
221
  /** Drop entries whose lockout has expired and whose failure count is 0 — keeps the map bounded. */
175
222
  private sweepExpired;
176
223
  /** TEST ONLY: number of tracked clients currently locked (lockedUntil in the future). */
@@ -178,10 +225,13 @@ declare class AuthGate {
178
225
  }
179
226
 
180
227
  interface SessionDefaults {
228
+ /** The provider used by the most recently created session. Absent only for legacy saved documents. */
229
+ provider?: ProviderId;
181
230
  effort: string;
182
231
  model?: string;
183
232
  dangerouslySkip: boolean;
184
233
  permissionMode?: string;
234
+ addDirs?: string[];
185
235
  codex?: {
186
236
  model?: string;
187
237
  reasoningEffort?: string;
@@ -235,6 +285,7 @@ interface StoredSessionBase {
235
285
  }
236
286
  interface StoredClaudeSession extends StoredSessionBase {
237
287
  provider: "claude";
288
+ externalAdapter?: never;
238
289
  dangerouslySkip: boolean;
239
290
  /** The USER-chosen spawn flags the session was created with (`--model`/`--effort`/`--permission-mode`/
240
291
  * `--dangerously-skip-permissions`/`--add-dir …`) — everything EXCEPT the ephemeral per-session
@@ -252,13 +303,24 @@ interface StoredIntegrationStatus {
252
303
  }
253
304
  interface StoredCodexSession extends StoredSessionBase {
254
305
  provider: "codex";
306
+ externalAdapter?: never;
255
307
  launchOptions: CodexSessionOptions;
256
308
  providerSessionId?: string;
257
309
  integrationStatus?: StoredIntegrationStatus;
258
310
  dangerouslySkip?: never;
259
311
  spawnArgs?: never;
260
312
  }
261
- type StoredSession = StoredClaudeSession | StoredCodexSession;
313
+ interface StoredExternalSession extends StoredSessionBase {
314
+ provider: ProviderId;
315
+ /** Discriminator that keeps additive third-party rows separate from rollback-readable built-in tables. */
316
+ externalAdapter: true;
317
+ launchOptions: ProviderSessionOptions;
318
+ providerSessionId?: string;
319
+ integrationStatus?: StoredIntegrationStatus;
320
+ dangerouslySkip?: never;
321
+ spawnArgs?: never;
322
+ }
323
+ type StoredSession = StoredClaudeSession | StoredCodexSession | StoredExternalSession;
262
324
  /** How a store is actually backed: "sqlite" (durable) or "memory-fallback" (the native module failed to
263
325
  * load — NOT durable across restarts). Surfaced so start.ts can warn loudly + /diag can report it. */
264
326
  type StoreMode = "sqlite" | "memory-fallback";
@@ -284,6 +346,8 @@ interface SessionStore {
284
346
  commitProvisionalProviderSessionId(id: string, value: string): void;
285
347
  getSessionDefaults(): StoredSessionDefaults | undefined;
286
348
  putSessionDefaults(defaults: SessionDefaults, expectedRevision: number, updatedAt: number): StoredSessionDefaults;
349
+ /** Server-owned last-launch write. Replaces the remembered choices and advances the revision atomically. */
350
+ rememberSessionDefaults(defaults: SessionDefaults, updatedAt: number): StoredSessionDefaults;
287
351
  putFile(file: StoredSessionFile): void;
288
352
  getFile(sessionId: string, id: string): StoredSessionFile | undefined;
289
353
  listFiles(sessionId: string, includeHidden?: boolean): StoredSessionFile[];
@@ -296,6 +360,15 @@ interface SessionStore {
296
360
  readonly mode: StoreMode;
297
361
  }
298
362
 
363
+ type DeviceScope = "direct" | "relay";
364
+ interface PairingTicket {
365
+ /** One-time capability carried by the pairing URL. Never persisted in plaintext. */
366
+ secret: string;
367
+ expiresAt: number;
368
+ /** Exact product surfaces the resulting device credential can reach. */
369
+ scopes: DeviceScope[];
370
+ }
371
+
299
372
  declare global {
300
373
  /** Stable package version injected by tsup. Source/test builds fall back to package.json. */
301
374
  const __SERVER_VERSION__: string | undefined;
@@ -322,9 +395,43 @@ type PaneStatus = "working" | "blocked" | "idle";
322
395
 
323
396
  declare class ProviderRegistry {
324
397
  private readonly byId;
398
+ private readonly manifests;
399
+ private readonly sources;
400
+ private readonly enabled;
325
401
  constructor(providers: readonly AgentProvider[]);
402
+ register(provider: AgentProvider, source?: "built-in" | "installed", enabled?: boolean): void;
403
+ setEnabled(id: ProviderId, enabled: boolean): void;
404
+ isEnabled(id: ProviderId): boolean;
405
+ source(id: ProviderId): "built-in" | "installed" | undefined;
406
+ unregisterInstalled(id: ProviderId): void;
326
407
  get(id: ProviderId): AgentProvider;
327
408
  list(): AgentProvider[];
409
+ listEnabled(): AgentProvider[];
410
+ manifest(id: ProviderId): Readonly<AdapterManifestV1>;
411
+ descriptors(): {
412
+ enabled: boolean;
413
+ id: string;
414
+ displayName: string;
415
+ version: string;
416
+ schemaVersion: 1;
417
+ source: "built-in" | "installed";
418
+ platforms: ("darwin" | "linux")[];
419
+ resumeIdentity: "optional" | "required" | "unsupported";
420
+ capabilities: {
421
+ usage: boolean;
422
+ state: boolean;
423
+ probe: boolean;
424
+ launch: boolean;
425
+ resume: boolean;
426
+ identity: boolean;
427
+ metadata: boolean;
428
+ login: boolean;
429
+ attachments: boolean;
430
+ cleanup: boolean;
431
+ };
432
+ stateAuthority: AdapterStateAuthority[];
433
+ optionSchema: Record<string, unknown>;
434
+ }[];
328
435
  }
329
436
 
330
437
  interface CodexSpawnLease {
@@ -454,6 +561,11 @@ interface TerminalManagerDeps {
454
561
  * are). Same never-throw contract.
455
562
  */
456
563
  onFinished?: (id: string, wasAttached: boolean) => void;
564
+ /** Every semantic state transition, including ones observed while a browser is attached. Used by the
565
+ * command-center event/attention layer; a failure is isolated from terminal state. */
566
+ onActivityChanged?: (id: string, previous: PaneStatus, current: PaneStatus, attached: boolean) => void;
567
+ /** A client focused/attached this terminal. Done-unseen attention can become seen; blocked attention stays. */
568
+ onViewed?: (id: string) => void;
457
569
  /**
458
570
  * Capture a tmux session's CURRENT rendered pane as plain text (READ-ONLY). Injected for tests; in
459
571
  * production it defaults to a real `capture-pane -p` on {@link TMUX_SOCKET}. Drives {@link refreshActivity},
@@ -466,15 +578,8 @@ interface TerminalManagerDeps {
466
578
  type CreateTerminalOptions = {
467
579
  id: string;
468
580
  cwd: string;
469
- provider: "claude";
470
- options: ClaudeSessionOptions;
471
- cols?: number;
472
- rows?: number;
473
- } | {
474
- id: string;
475
- cwd: string;
476
- provider: "codex";
477
- options: CodexSessionOptions;
581
+ provider: ProviderId;
582
+ options: ProviderSessionOptions;
478
583
  cols?: number;
479
584
  rows?: number;
480
585
  };
@@ -491,6 +596,7 @@ declare class TerminalManager {
491
596
  private readonly providers;
492
597
  private attachConfig?;
493
598
  constructor(deps: TerminalManagerDeps);
599
+ private notifyActivityChanged;
494
600
  /** Late-bound (after listen(), which resolves the loopback port) — same config the chat SessionManager
495
601
  * gets. When set, each terminal's claude is spawned with `--mcp-config` so send_image/send_file work. */
496
602
  setAttachConfig(attach: AttachSpawnOptions | undefined): void;
@@ -599,16 +705,404 @@ declare class TerminalManager {
599
705
  private killTmux;
600
706
  }
601
707
 
708
+ type InputLeaseActorType = "device" | "host" | "local" | "relay";
709
+ interface InputLeasePrincipal {
710
+ actorType: InputLeaseActorType;
711
+ actorId: string;
712
+ label: string;
713
+ }
714
+ interface InputLease {
715
+ id: string;
716
+ sessionId: string;
717
+ holderId: string;
718
+ actorType: InputLeaseActorType;
719
+ actorId: string;
720
+ label: string;
721
+ acquiredAt: number;
722
+ renewedAt: number;
723
+ expiresAt: number;
724
+ revision: number;
725
+ }
726
+ type InputLeaseEvent = {
727
+ type: "granted";
728
+ lease: InputLease;
729
+ } | {
730
+ type: "renewed";
731
+ lease: InputLease;
732
+ } | {
733
+ type: "released" | "expired" | "revoked";
734
+ lease: InputLease;
735
+ } | {
736
+ type: "taken-over";
737
+ lease: InputLease;
738
+ previous: InputLease;
739
+ };
740
+ type InputLeaseAcquireResult = {
741
+ status: "granted" | "owned";
742
+ lease: InputLease;
743
+ } | {
744
+ status: "denied";
745
+ current: InputLease;
746
+ };
747
+ interface InputLeaseCoordinatorOptions {
748
+ ttlMs?: number;
749
+ now?: () => number;
750
+ generateId?: () => string;
751
+ scheduleExpiry?: boolean;
752
+ onEvent?: (event: InputLeaseEvent) => void;
753
+ }
754
+ /**
755
+ * Coordinates the one mutable terminal input stream shared by many read-only observers.
756
+ *
757
+ * Leases intentionally live in memory: a host restart invalidates every connection and therefore every holder.
758
+ * Durable audit is supplied by the caller through onEvent. Expiry, release, and confirmed takeover are explicit;
759
+ * merely sending input can never acquire or steal ownership.
760
+ */
761
+ declare class InputLeaseCoordinator {
762
+ private readonly leases;
763
+ private readonly listeners;
764
+ private readonly timers;
765
+ private readonly ttlMs;
766
+ private readonly now;
767
+ private readonly generateId;
768
+ private readonly scheduleExpiry;
769
+ private readonly onEvent?;
770
+ private revision;
771
+ constructor(options?: InputLeaseCoordinatorOptions);
772
+ acquire(sessionId: string, holderId: string, principal: InputLeasePrincipal): InputLeaseAcquireResult;
773
+ takeover(sessionId: string, holderId: string, principal: InputLeasePrincipal, confirmed: boolean, authorized?: boolean): InputLeaseAcquireResult;
774
+ renew(sessionId: string, holderId: string, leaseId: string): InputLease | undefined;
775
+ canWrite(sessionId: string, holderId: string, leaseId?: string): boolean;
776
+ release(sessionId: string, holderId: string, leaseId?: string): boolean;
777
+ releaseHolder(holderId: string): number;
778
+ revoke(sessionId: string): boolean;
779
+ revokeActor(actorType: InputLeaseActorType, actorId: string): number;
780
+ get(sessionId: string): InputLease | undefined;
781
+ subscribe(sessionId: string, listener: (event: InputLeaseEvent) => void): () => void;
782
+ close(): void;
783
+ private validate;
784
+ private current;
785
+ private renewExisting;
786
+ private remove;
787
+ private schedule;
788
+ private emit;
789
+ }
790
+
791
+ type TeamStoreMode = "sqlite" | "memory-fallback";
792
+ type TeamMemberKind = "person" | "service";
793
+ type TeamMemberStatus = "active" | "suspended" | "removed";
794
+ type TeamRole = "viewer" | "operator" | "workspace-manager" | "extension-manager" | "policy-admin" | "organization-admin";
795
+ type TeamScopeType = "team" | "host" | "workspace";
796
+ type TeamPrincipalType = "device" | "host" | "local" | "relay";
797
+ type TeamPermission = "team:read" | "sessions:read" | "sessions:operate" | "attention:read" | "attention:manage" | "presence:read" | "presence:write" | "workspaces:manage" | "extensions:manage" | "policy:manage" | "members:manage" | "audit:read" | "fleet:read";
798
+ interface TeamRecord {
799
+ id: string;
800
+ name: string;
801
+ authorizationEnabled: boolean;
802
+ revision: number;
803
+ createdAt: number;
804
+ updatedAt: number;
805
+ }
806
+ interface TeamMember {
807
+ id: string;
808
+ displayName: string;
809
+ kind: TeamMemberKind;
810
+ status: TeamMemberStatus;
811
+ revision: number;
812
+ createdAt: number;
813
+ updatedAt: number;
814
+ }
815
+ interface TeamRoleBinding {
816
+ id: string;
817
+ memberId: string;
818
+ role: TeamRole;
819
+ scopeType: TeamScopeType;
820
+ scopeId?: string;
821
+ createdAt: number;
822
+ }
823
+ interface TeamPrincipalBinding {
824
+ actorType: TeamPrincipalType;
825
+ actorId: string;
826
+ memberId: string;
827
+ createdAt: number;
828
+ }
829
+ interface TeamAuthorizationResource {
830
+ hostId?: string;
831
+ workspaceId?: string;
832
+ }
833
+ interface TeamAuthorizationDecision {
834
+ allowed: boolean;
835
+ reason: "local-break-glass" | "not-enforced" | "unbound" | "inactive" | "role" | "missing-permission";
836
+ member?: TeamMember;
837
+ roles: TeamRole[];
838
+ }
839
+ interface TeamStore {
840
+ readonly mode: TeamStoreMode;
841
+ getTeam(): TeamRecord | null;
842
+ createTeam(input: {
843
+ name: string;
844
+ ownerName: string;
845
+ ownerPrincipal: {
846
+ actorType: TeamPrincipalType;
847
+ actorId: string;
848
+ };
849
+ }, now?: number): {
850
+ team: TeamRecord;
851
+ owner: TeamMember;
852
+ };
853
+ updateTeam(input: {
854
+ name?: string;
855
+ authorizationEnabled?: boolean;
856
+ }, expectedRevision: number, now?: number): TeamRecord;
857
+ listMembers(opts?: {
858
+ includeRemoved?: boolean;
859
+ }): TeamMember[];
860
+ getMember(id: string): TeamMember | undefined;
861
+ createMember(input: {
862
+ displayName: string;
863
+ kind?: TeamMemberKind;
864
+ }, now?: number): TeamMember;
865
+ updateMember(id: string, input: {
866
+ displayName?: string;
867
+ status?: TeamMemberStatus;
868
+ }, expectedRevision: number, now?: number): TeamMember | undefined;
869
+ listRoleBindings(memberId?: string): TeamRoleBinding[];
870
+ grantRole(input: {
871
+ memberId: string;
872
+ role: TeamRole;
873
+ scopeType?: TeamScopeType;
874
+ scopeId?: string;
875
+ }, now?: number): TeamRoleBinding;
876
+ revokeRole(id: string, now?: number): boolean;
877
+ listPrincipalBindings(memberId?: string): TeamPrincipalBinding[];
878
+ bindPrincipal(input: {
879
+ memberId: string;
880
+ actorType: TeamPrincipalType;
881
+ actorId: string;
882
+ }, now?: number): TeamPrincipalBinding;
883
+ unbindPrincipal(actorType: TeamPrincipalType, actorId: string, now?: number): boolean;
884
+ memberForPrincipal(actorType: TeamPrincipalType, actorId: string): TeamMember | undefined;
885
+ authorize(actorType: TeamPrincipalType, actorId: string, permission: TeamPermission, resource?: TeamAuthorizationResource): TeamAuthorizationDecision;
886
+ close(): void;
887
+ }
888
+
889
+ type PolicyStoreMode = "sqlite" | "memory-fallback";
890
+ type ExtensionPolicyMode = "allow-integrity" | "signed-only" | "deny";
891
+ type UpdatePolicyMode = "stable-only" | "deny";
892
+ interface EnterprisePolicy {
893
+ enforcementEnabled: boolean;
894
+ allowedHostIds: string[] | null;
895
+ allowedWorkspaceIds: string[] | null;
896
+ allowedProviderIds: string[] | null;
897
+ allowDangerousProviderModes: boolean;
898
+ allowFileTransfer: boolean;
899
+ extensionMode: ExtensionPolicyMode;
900
+ allowRelay: boolean;
901
+ updateMode: UpdatePolicyMode;
902
+ revision: number;
903
+ createdAt: number;
904
+ updatedAt: number;
905
+ }
906
+ interface EnterprisePolicyUpdate {
907
+ enforcementEnabled?: boolean;
908
+ allowedHostIds?: string[] | null;
909
+ allowedWorkspaceIds?: string[] | null;
910
+ allowedProviderIds?: string[] | null;
911
+ allowDangerousProviderModes?: boolean;
912
+ allowFileTransfer?: boolean;
913
+ extensionMode?: ExtensionPolicyMode;
914
+ allowRelay?: boolean;
915
+ updateMode?: UpdatePolicyMode;
916
+ }
917
+ interface PolicyStore {
918
+ readonly mode: PolicyStoreMode;
919
+ get(): EnterprisePolicy;
920
+ update(input: EnterprisePolicyUpdate, expectedRevision: number, now?: number): EnterprisePolicy;
921
+ close(): void;
922
+ }
923
+
924
+ type PeerStoreMode = "sqlite" | "memory-fallback";
925
+ type PeerAction = "read" | "send" | "wait" | "start" | "focus";
926
+ type PeerStatus = "active" | "suspended";
927
+ interface PeerRecord {
928
+ id: string;
929
+ label: string;
930
+ remoteHostId: string;
931
+ remoteVersion: string;
932
+ actions: PeerAction[];
933
+ allowedWorkspaceIds: string[] | null;
934
+ status: PeerStatus;
935
+ revision: number;
936
+ createdAt: number;
937
+ updatedAt: number;
938
+ lastVerifiedAt: number;
939
+ }
940
+ interface PeerConnection extends PeerRecord {
941
+ baseUrl: string;
942
+ credential: string;
943
+ }
944
+ interface CreatePeerInput {
945
+ label: string;
946
+ baseUrl: string;
947
+ credential: string;
948
+ remoteHostId: string;
949
+ remoteVersion: string;
950
+ actions?: PeerAction[];
951
+ allowedWorkspaceIds?: string[] | null;
952
+ }
953
+ interface UpdatePeerInput {
954
+ label?: string;
955
+ actions?: PeerAction[];
956
+ allowedWorkspaceIds?: string[] | null;
957
+ status?: PeerStatus;
958
+ remoteVersion?: string;
959
+ lastVerifiedAt?: number;
960
+ }
961
+ interface PeerStore {
962
+ readonly mode: PeerStoreMode;
963
+ list(): PeerRecord[];
964
+ get(id: string): PeerRecord | undefined;
965
+ connection(id: string): PeerConnection | undefined;
966
+ create(input: CreatePeerInput, now?: number): PeerRecord;
967
+ update(id: string, input: UpdatePeerInput, expectedRevision: number, now?: number): PeerRecord | undefined;
968
+ rotateCredential(id: string, input: {
969
+ credential: string;
970
+ remoteVersion: string;
971
+ lastVerifiedAt?: number;
972
+ }, expectedRevision: number, now?: number): PeerRecord | undefined;
973
+ remove(id: string): boolean;
974
+ close(): void;
975
+ }
976
+
977
+ type PresenceMode = "viewing" | "operating";
978
+ interface PresenceTarget {
979
+ hostId: string;
980
+ workspaceId?: string;
981
+ sessionId?: string;
982
+ agentId?: string;
983
+ }
984
+ interface PresenceRecord extends PresenceTarget {
985
+ id: string;
986
+ memberId?: string;
987
+ label: string;
988
+ mode: PresenceMode;
989
+ connectedAt: number;
990
+ lastSeenAt: number;
991
+ expiresAt: number;
992
+ revision: number;
993
+ }
994
+ type PresenceEvent = {
995
+ type: "joined" | "updated" | "left" | "expired";
996
+ presence: PresenceRecord;
997
+ };
998
+ interface PresenceHeartbeatInput extends PresenceTarget {
999
+ clientId: string;
1000
+ mode: PresenceMode;
1001
+ memberId?: string;
1002
+ }
1003
+ interface PresenceCoordinatorOptions {
1004
+ ttlMs?: number;
1005
+ now?: () => number;
1006
+ generateId?: () => string;
1007
+ maxRecords?: number;
1008
+ maxPerActor?: number;
1009
+ scheduleExpiry?: boolean;
1010
+ }
1011
+ /**
1012
+ * Ephemeral, privacy-bounded presence. No IP, bearer credential, path, prompt, or terminal data is retained.
1013
+ * Heartbeats expire cleanly after disconnect and are capped both globally and per authenticated actor.
1014
+ */
1015
+ declare class PresenceCoordinator {
1016
+ private readonly records;
1017
+ private readonly listeners;
1018
+ private readonly ttlMs;
1019
+ private readonly now;
1020
+ private readonly generateId;
1021
+ private readonly maxRecords;
1022
+ private readonly maxPerActor;
1023
+ private readonly timer?;
1024
+ private revision;
1025
+ constructor(options?: PresenceCoordinatorOptions);
1026
+ heartbeat(principal: InputLeasePrincipal, input: PresenceHeartbeatInput): PresenceRecord;
1027
+ list(filter?: Partial<PresenceTarget>): PresenceRecord[];
1028
+ release(principal: Pick<InputLeasePrincipal, "actorType" | "actorId">, clientId: string): boolean;
1029
+ releaseActor(principal: Pick<InputLeasePrincipal, "actorType" | "actorId">): number;
1030
+ /**
1031
+ * Input ownership is authoritative for the public "operating" label. When a lease ends or moves, keep the
1032
+ * connected observer visible but immediately downgrade stale operating heartbeats instead of waiting for TTL.
1033
+ */
1034
+ downgradeOperating(principal: Pick<InputLeasePrincipal, "actorType" | "actorId">, sessionId: string): number;
1035
+ subscribe(listener: (event: PresenceEvent) => void): () => void;
1036
+ sweep(): number;
1037
+ close(): void;
1038
+ private enforceBounds;
1039
+ private evict;
1040
+ private publicRecord;
1041
+ private emit;
1042
+ }
1043
+
1044
+ type RelayRpcMethod = "GET" | "HEAD" | "POST" | "PUT" | "PATCH" | "DELETE";
1045
+ interface RelayRpcRequest {
1046
+ id: string;
1047
+ method: RelayRpcMethod;
1048
+ path: string;
1049
+ headers: Record<string, string>;
1050
+ body?: Buffer;
1051
+ }
1052
+ interface RelayRpcResponse {
1053
+ id: string;
1054
+ status: number;
1055
+ headers: Record<string, string>;
1056
+ body?: string;
1057
+ }
1058
+
602
1059
  interface CreateServerResult {
603
1060
  app: FastifyInstance;
604
1061
  authGate: AuthGate;
1062
+ /** Issue a five-minute, one-use pairing capability without exposing the host's master token. */
1063
+ issuePairing(): PairingTicket;
1064
+ /** Internal E2E-relay bridge. It authenticates relay-scoped devices through the exact normal route hooks. */
1065
+ dispatchRelayRequest(token: string, request: RelayRpcRequest): Promise<RelayRpcResponse>;
1066
+ /** Issues a relay-principal terminal ticket without exposing the bridge's process-local capability. */
1067
+ issueRelayTerminalTicket(token: string): Promise<string>;
1068
+ /** Process-local loopback headers for streamed relay HTTP. Never expose these outside this server process. */
1069
+ relayLoopbackHeaders(token: string, headers?: Record<string, string>): Record<string, string>;
605
1070
  /** Exposed so startServer can late-bind the MCP attach config (after listen() resolves the port) —
606
1071
  * this is what gives the terminal's claude send_image/send_file. */
607
1072
  terminalManager: TerminalManager;
1073
+ /** Exposed for relay/team composition and isolated ownership tests. */
1074
+ inputLeases: InputLeaseCoordinator;
1075
+ teamStore: TeamStore;
1076
+ policyStore: PolicyStore;
1077
+ peerStore: PeerStore;
1078
+ presence: PresenceCoordinator;
608
1079
  /** False when tmux/node-pty is unavailable → terminal sessions are disabled (startServer warns loudly). */
609
1080
  terminalAvailable: boolean;
610
1081
  }
611
1082
 
1083
+ type RelayHostStatus = "idle" | "connecting" | "online" | "reconnecting" | "stopped";
1084
+ interface RelayHostMetrics {
1085
+ status: RelayHostStatus;
1086
+ activeChannels: number;
1087
+ acceptedChannels: number;
1088
+ rejectedChannels: number;
1089
+ completedRequests: number;
1090
+ failedRequests: number;
1091
+ reconnects: number;
1092
+ activeTransfers: number;
1093
+ completedTransfers: number;
1094
+ failedTransfers: number;
1095
+ streamedRequestBytes: number;
1096
+ streamedResponseBytes: number;
1097
+ }
1098
+ interface RelayHostConnector {
1099
+ start(): void;
1100
+ stop(): Promise<void>;
1101
+ waitUntilReady(timeoutMs?: number): Promise<void>;
1102
+ closeDevice(deviceId: string): void;
1103
+ metrics(): RelayHostMetrics;
1104
+ }
1105
+
612
1106
  declare function providerPreflightWarning(name: string, availability: ProviderAvailability): string | undefined;
613
1107
  declare function runProviderPreflight(providers: ReadonlyArray<{
614
1108
  name: string;
@@ -631,11 +1125,7 @@ declare function startServer(env?: NodeJS.ProcessEnv): Promise<CreateServerResul
631
1125
  url: string;
632
1126
  token?: string;
633
1127
  tokenGenerated: boolean;
1128
+ relayHost?: RelayHostConnector;
634
1129
  }>;
635
- /** Install process-wide crash guards so a stray unhandled rejection or a listener-less EventEmitter
636
- * `error` (e.g. a write-after-teardown on a dying claude child, or a detached updater spawn failure)
637
- * LOGS instead of taking the whole server down — for an always-on self-hosted server, staying up beats
638
- * crashing. Install ONCE at a process entry (never in startServer, which tests call repeatedly). */
639
- declare function installCrashGuards(): void;
640
1130
 
641
- export { claudePreflightWarning, installCrashGuards, providerPreflightWarning, runClaudePreflight, runProviderPreflight, startServer };
1131
+ export { claudePreflightWarning, providerPreflightWarning, runClaudePreflight, runProviderPreflight, startServer };