@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/container/relay.js +1647 -0
- package/dist/health-watchdog.js +87 -0
- package/dist/index.d.ts +1775 -20
- package/dist/index.js +18085 -3812
- package/dist/managed-update-helper.js +4 -1
- package/dist/relay-start.d.ts +118 -0
- package/dist/relay-start.js +1642 -0
- package/dist/start.d.ts +509 -19
- package/dist/start.js +17254 -4741
- package/package.json +6 -5
package/dist/start.d.ts
CHANGED
|
@@ -18,7 +18,26 @@ interface AttachSpawnOptions {
|
|
|
18
18
|
dataDir: string;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
470
|
-
options:
|
|
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,
|
|
1131
|
+
export { claudePreflightWarning, providerPreflightWarning, runClaudePreflight, runProviderPreflight, startServer };
|