@roamcode.ai/server 1.0.23 → 1.2.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). */
@@ -238,6 +285,7 @@ interface StoredSessionBase {
238
285
  }
239
286
  interface StoredClaudeSession extends StoredSessionBase {
240
287
  provider: "claude";
288
+ externalAdapter?: never;
241
289
  dangerouslySkip: boolean;
242
290
  /** The USER-chosen spawn flags the session was created with (`--model`/`--effort`/`--permission-mode`/
243
291
  * `--dangerously-skip-permissions`/`--add-dir …`) — everything EXCEPT the ephemeral per-session
@@ -255,13 +303,24 @@ interface StoredIntegrationStatus {
255
303
  }
256
304
  interface StoredCodexSession extends StoredSessionBase {
257
305
  provider: "codex";
306
+ externalAdapter?: never;
258
307
  launchOptions: CodexSessionOptions;
259
308
  providerSessionId?: string;
260
309
  integrationStatus?: StoredIntegrationStatus;
261
310
  dangerouslySkip?: never;
262
311
  spawnArgs?: never;
263
312
  }
264
- 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;
265
324
  /** How a store is actually backed: "sqlite" (durable) or "memory-fallback" (the native module failed to
266
325
  * load — NOT durable across restarts). Surfaced so start.ts can warn loudly + /diag can report it. */
267
326
  type StoreMode = "sqlite" | "memory-fallback";
@@ -301,6 +360,15 @@ interface SessionStore {
301
360
  readonly mode: StoreMode;
302
361
  }
303
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
+
304
372
  declare global {
305
373
  /** Stable package version injected by tsup. Source/test builds fall back to package.json. */
306
374
  const __SERVER_VERSION__: string | undefined;
@@ -327,9 +395,43 @@ type PaneStatus = "working" | "blocked" | "idle";
327
395
 
328
396
  declare class ProviderRegistry {
329
397
  private readonly byId;
398
+ private readonly manifests;
399
+ private readonly sources;
400
+ private readonly enabled;
330
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;
331
407
  get(id: ProviderId): AgentProvider;
332
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
+ }[];
333
435
  }
334
436
 
335
437
  interface CodexSpawnLease {
@@ -459,6 +561,11 @@ interface TerminalManagerDeps {
459
561
  * are). Same never-throw contract.
460
562
  */
461
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;
462
569
  /**
463
570
  * Capture a tmux session's CURRENT rendered pane as plain text (READ-ONLY). Injected for tests; in
464
571
  * production it defaults to a real `capture-pane -p` on {@link TMUX_SOCKET}. Drives {@link refreshActivity},
@@ -471,15 +578,8 @@ interface TerminalManagerDeps {
471
578
  type CreateTerminalOptions = {
472
579
  id: string;
473
580
  cwd: string;
474
- provider: "claude";
475
- options: ClaudeSessionOptions;
476
- cols?: number;
477
- rows?: number;
478
- } | {
479
- id: string;
480
- cwd: string;
481
- provider: "codex";
482
- options: CodexSessionOptions;
581
+ provider: ProviderId;
582
+ options: ProviderSessionOptions;
483
583
  cols?: number;
484
584
  rows?: number;
485
585
  };
@@ -496,6 +596,7 @@ declare class TerminalManager {
496
596
  private readonly providers;
497
597
  private attachConfig?;
498
598
  constructor(deps: TerminalManagerDeps);
599
+ private notifyActivityChanged;
499
600
  /** Late-bound (after listen(), which resolves the loopback port) — same config the chat SessionManager
500
601
  * gets. When set, each terminal's claude is spawned with `--mcp-config` so send_image/send_file work. */
501
602
  setAttachConfig(attach: AttachSpawnOptions | undefined): void;
@@ -604,16 +705,404 @@ declare class TerminalManager {
604
705
  private killTmux;
605
706
  }
606
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
+
607
1059
  interface CreateServerResult {
608
1060
  app: FastifyInstance;
609
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>;
610
1070
  /** Exposed so startServer can late-bind the MCP attach config (after listen() resolves the port) —
611
1071
  * this is what gives the terminal's claude send_image/send_file. */
612
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;
613
1079
  /** False when tmux/node-pty is unavailable → terminal sessions are disabled (startServer warns loudly). */
614
1080
  terminalAvailable: boolean;
615
1081
  }
616
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
+
617
1106
  declare function providerPreflightWarning(name: string, availability: ProviderAvailability): string | undefined;
618
1107
  declare function runProviderPreflight(providers: ReadonlyArray<{
619
1108
  name: string;
@@ -636,11 +1125,7 @@ declare function startServer(env?: NodeJS.ProcessEnv): Promise<CreateServerResul
636
1125
  url: string;
637
1126
  token?: string;
638
1127
  tokenGenerated: boolean;
1128
+ relayHost?: RelayHostConnector;
639
1129
  }>;
640
- /** Install process-wide crash guards so a stray unhandled rejection or a listener-less EventEmitter
641
- * `error` (e.g. a write-after-teardown on a dying claude child, or a detached updater spawn failure)
642
- * LOGS instead of taking the whole server down — for an always-on self-hosted server, staying up beats
643
- * crashing. Install ONCE at a process entry (never in startServer, which tests call repeatedly). */
644
- declare function installCrashGuards(): void;
645
1130
 
646
- export { claudePreflightWarning, installCrashGuards, providerPreflightWarning, runClaudePreflight, runProviderPreflight, startServer };
1131
+ export { claudePreflightWarning, providerPreflightWarning, runClaudePreflight, runProviderPreflight, startServer };