@sealant/sdk 0.21.0 → 0.23.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/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AccessTokensNamespace, CreateOptions, InferenceNamespace, ListOptions, Run, Workspace, SealantConfig } from "./types.js";
1
+ import type { AccessTokensNamespace, ConnectedAccountsNamespace, CreateOptions, InferenceNamespace, ListOptions, Run, Workspace, SealantConfig, UsersNamespace } from "./types.js";
2
2
  export declare class Sealant {
3
3
  #private;
4
4
  constructor(config: SealantConfig);
@@ -21,6 +21,10 @@ export declare class Sealant {
21
21
  * the session endpoints enforce exactly those scopes (a read-stream token can stream but is
22
22
  * rejected for input and exec).
23
23
  */
24
+ /** Identity rows for products that own their own login (one client per user afterwards). */
25
+ readonly users: UsersNamespace;
26
+ /** This client's owner's Claude / Codex / GitHub accounts. Secrets go in; none come out. */
27
+ readonly connectedAccounts: ConnectedAccountsNamespace;
24
28
  readonly accessTokens: AccessTokensNamespace;
25
29
  /** Runs by id — so a record can be replayed long after its workspace is gone. */
26
30
  readonly runs: {
package/dist/client.js CHANGED
@@ -1,16 +1,4 @@
1
- /**
2
- * `Sealant` — the public client and the import users reach for.
3
- *
4
- * import { Sealant, opencode } from "@sealant/sdk"
5
- * const sealant = new Sealant({ baseUrl: "http://localhost:8080" })
6
- *
7
- * This is the plain-Promise facade over the Effect core: an app `Layer` built once in the
8
- * constructor providing the wire client derived from `@sealant/api-contracts`. Everything is a plain
9
- * HTTP call to `baseUrl`. Operations whose endpoints have not landed yet stay TYPED against the
10
- * stable surface and reject with `SealantNotImplementedError` so callers can compile and wire
11
- * against the final shape today.
12
- */
13
- import { createAccessTokenOp, createWorkspaceOp, getRunOp, getWorkspaceOp, inferenceRespondOp, listWorkspacesOp, } from "./effect/operations.js";
1
+ import { archiveConnectedAccountOp, createAccessTokenOp, createConnectedAccountOp, createWorkspaceOp, ensureUserOp, getRunOp, getUserOp, getWorkspaceOp, inferenceRespondOp, listConnectedAccountsOp, listWorkspacesOp, } from "./effect/operations.js";
14
2
  import { runHarness, startHarness } from "./effect/run-harness.js";
15
3
  import { makeSdkRuntime } from "./effect/runtime.js";
16
4
  import { SealantError } from "./errors.js";
@@ -20,6 +8,19 @@ import { buildCreateWorkspaceRequest } from "./internal/blueprint.js";
20
8
  import { resolveInternalConfig } from "./internal/config.js";
21
9
  import { parseTtlSeconds } from "./internal/duration.js";
22
10
  import { buildInferenceRespondRequest, mapInferenceResponse } from "./internal/inference.js";
11
+ const mapConnectedAccount = (wire) => ({
12
+ connectedAccountId: wire.connectedAccountId,
13
+ ownerUserId: wire.ownerUserId,
14
+ provider: wire.provider,
15
+ name: wire.name,
16
+ kind: wire.kind,
17
+ status: wire.status,
18
+ metadata: wire.metadata,
19
+ connectedAt: wire.connectedAt,
20
+ updatedAt: wire.updatedAt,
21
+ lastUsedAt: wire.lastUsedAt,
22
+ lastSyncedAt: wire.lastSyncedAt,
23
+ });
23
24
  // Wire the run-execution implementations into the Workspace facade (the injection point exists to
24
25
  // break the workspace <-> run-harness import cycle; the client is the composition root).
25
26
  registerHarnessExecutors({ run: runHarness, start: startHarness });
@@ -72,7 +73,7 @@ export class Sealant {
72
73
  return workspace.ready();
73
74
  },
74
75
  get: async (id) => {
75
- const details = await this.#runtime.run(getWorkspaceOp(id));
76
+ const details = await this.#runtime.run(getWorkspaceOp(id, this.#ctx.config.hostLocal.ownerUserId));
76
77
  return makeWorkspace(this.#ctx, {
77
78
  id: details.workspaceId,
78
79
  name: details.name,
@@ -109,6 +110,47 @@ export class Sealant {
109
110
  * the session endpoints enforce exactly those scopes (a read-stream token can stream but is
110
111
  * rejected for input and exec).
111
112
  */
113
+ /** Identity rows for products that own their own login (one client per user afterwards). */
114
+ users = {
115
+ ensure: async (options) => {
116
+ const wire = await this.#runtime.run(ensureUserOp({
117
+ email: options.email,
118
+ name: options.name,
119
+ ...(options.userId === undefined ? {} : { userId: options.userId }),
120
+ }));
121
+ return {
122
+ userId: wire.userId,
123
+ email: wire.email,
124
+ name: wire.name,
125
+ createdAt: wire.createdAt,
126
+ created: wire.created,
127
+ };
128
+ },
129
+ get: async (userId) => {
130
+ const wire = await this.#runtime.run(getUserOp(userId));
131
+ return { userId: wire.userId, email: wire.email, name: wire.name, createdAt: wire.createdAt };
132
+ },
133
+ };
134
+ /** This client's owner's Claude / Codex / GitHub accounts. Secrets go in; none come out. */
135
+ connectedAccounts = {
136
+ list: async () => {
137
+ const wire = await this.#runtime.run(listConnectedAccountsOp(this.#ctx.config.hostLocal.ownerUserId));
138
+ return wire.items.map(mapConnectedAccount);
139
+ },
140
+ connect: async (options) => {
141
+ const wire = await this.#runtime.run(createConnectedAccountOp({
142
+ ownerUserId: this.#ctx.config.hostLocal.ownerUserId,
143
+ provider: options.provider,
144
+ secret: options.secret,
145
+ ...(options.name === undefined ? {} : { name: options.name }),
146
+ }));
147
+ return mapConnectedAccount(wire);
148
+ },
149
+ disconnect: async (connectedAccountId) => {
150
+ const wire = await this.#runtime.run(archiveConnectedAccountOp(connectedAccountId, this.#ctx.config.hostLocal.ownerUserId));
151
+ return mapConnectedAccount(wire);
152
+ },
153
+ };
112
154
  accessTokens = {
113
155
  create: async (options) => {
114
156
  const wire = await this.#runtime.run(createAccessTokenOp({
@@ -130,7 +172,7 @@ export class Sealant {
130
172
  /** Runs by id — so a record can be replayed long after its workspace is gone. */
131
173
  runs = {
132
174
  get: async (runId) => {
133
- const wire = await this.#runtime.run(getRunOp(runId));
175
+ const wire = await this.#runtime.run(getRunOp(runId, this.#ctx.config.hostLocal.ownerUserId));
134
176
  return makeRun(this.#ctx, { wire });
135
177
  },
136
178
  };
@@ -537,6 +537,9 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
537
537
  readonly params: {
538
538
  readonly runId: string;
539
539
  };
540
+ readonly query: {
541
+ readonly ownerUserId?: string | undefined;
542
+ };
540
543
  readonly responseMode?: Mode;
541
544
  }) => Effect.Effect<HttpApiClient.Client.Response<{
542
545
  readonly runId: string;
@@ -566,6 +569,9 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
566
569
  readonly params: {
567
570
  readonly runId: string;
568
571
  };
572
+ readonly query: {
573
+ readonly ownerUserId?: string | undefined;
574
+ };
569
575
  readonly responseMode?: Mode;
570
576
  }) => Effect.Effect<HttpApiClient.Client.Response<{
571
577
  readonly files: readonly {
@@ -580,6 +586,9 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
580
586
  readonly runId: string;
581
587
  readonly sequence: string;
582
588
  };
589
+ readonly query: {
590
+ readonly ownerUserId?: string | undefined;
591
+ };
583
592
  readonly responseMode?: Mode;
584
593
  }) => Effect.Effect<HttpApiClient.Client.Response<{
585
594
  readonly eventId: string;
@@ -603,6 +612,9 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
603
612
  readonly params: {
604
613
  readonly runId: string;
605
614
  };
615
+ readonly query: {
616
+ readonly ownerUserId?: string | undefined;
617
+ };
606
618
  readonly responseMode?: Mode;
607
619
  }) => Effect.Effect<HttpApiClient.Client.Response<{
608
620
  readonly runId: string;
@@ -624,6 +636,7 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
624
636
  readonly runId: string;
625
637
  };
626
638
  readonly query: {
639
+ readonly ownerUserId?: string | undefined;
627
640
  readonly processId: string;
628
641
  readonly stream: "pty" | "stderr" | "stdout";
629
642
  readonly atSequence?: string | undefined;
@@ -642,6 +655,7 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
642
655
  readonly runId: string;
643
656
  };
644
657
  readonly query: {
658
+ readonly ownerUserId?: string | undefined;
645
659
  readonly fromSequence?: string | undefined;
646
660
  readonly toSequence?: string | undefined;
647
661
  readonly limit?: string | undefined;
@@ -1038,6 +1052,33 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
1038
1052
  readonly status: "ok";
1039
1053
  }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError), [Mode] extends ["response-only"] ? never : never>;
1040
1054
  };
1055
+ readonly users: {
1056
+ readonly ensureUser: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
1057
+ readonly payload: {
1058
+ readonly email: string;
1059
+ readonly name: string;
1060
+ readonly userId?: string | undefined;
1061
+ };
1062
+ readonly responseMode?: Mode;
1063
+ }) => Effect.Effect<HttpApiClient.Client.Response<{
1064
+ readonly userId: string;
1065
+ readonly email: string;
1066
+ readonly name: string;
1067
+ readonly createdAt: string;
1068
+ readonly created: boolean;
1069
+ }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").UserBadRequestError | import("@sealant/api-contracts").UserInternalServerError), [Mode] extends ["response-only"] ? never : never>;
1070
+ readonly getUser: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
1071
+ readonly params: {
1072
+ readonly userId: string;
1073
+ };
1074
+ readonly responseMode?: Mode;
1075
+ }) => Effect.Effect<HttpApiClient.Client.Response<{
1076
+ readonly userId: string;
1077
+ readonly email: string;
1078
+ readonly name: string;
1079
+ readonly createdAt: string;
1080
+ }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").UserInternalServerError | import("@sealant/api-contracts").UserNotFoundError), [Mode] extends ["response-only"] ? never : never>;
1081
+ };
1041
1082
  readonly workspaces: {
1042
1083
  readonly createWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
1043
1084
  readonly headers: {
@@ -1136,6 +1177,9 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
1136
1177
  readonly params: {
1137
1178
  readonly workspaceId: string;
1138
1179
  };
1180
+ readonly query: {
1181
+ readonly ownerUserId?: string | undefined;
1182
+ };
1139
1183
  readonly responseMode?: Mode;
1140
1184
  }) => Effect.Effect<HttpApiClient.Client.Response<{
1141
1185
  readonly workspaceId: string;
@@ -1863,6 +1907,9 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
1863
1907
  readonly params: {
1864
1908
  readonly runId: string;
1865
1909
  };
1910
+ readonly query: {
1911
+ readonly ownerUserId?: string | undefined;
1912
+ };
1866
1913
  readonly responseMode?: Mode;
1867
1914
  }) => Effect.Effect<HttpApiClient.Client.Response<{
1868
1915
  readonly runId: string;
@@ -1892,6 +1939,9 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
1892
1939
  readonly params: {
1893
1940
  readonly runId: string;
1894
1941
  };
1942
+ readonly query: {
1943
+ readonly ownerUserId?: string | undefined;
1944
+ };
1895
1945
  readonly responseMode?: Mode;
1896
1946
  }) => Effect.Effect<HttpApiClient.Client.Response<{
1897
1947
  readonly files: readonly {
@@ -1906,6 +1956,9 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
1906
1956
  readonly runId: string;
1907
1957
  readonly sequence: string;
1908
1958
  };
1959
+ readonly query: {
1960
+ readonly ownerUserId?: string | undefined;
1961
+ };
1909
1962
  readonly responseMode?: Mode;
1910
1963
  }) => Effect.Effect<HttpApiClient.Client.Response<{
1911
1964
  readonly eventId: string;
@@ -1929,6 +1982,9 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
1929
1982
  readonly params: {
1930
1983
  readonly runId: string;
1931
1984
  };
1985
+ readonly query: {
1986
+ readonly ownerUserId?: string | undefined;
1987
+ };
1932
1988
  readonly responseMode?: Mode;
1933
1989
  }) => Effect.Effect<HttpApiClient.Client.Response<{
1934
1990
  readonly runId: string;
@@ -1950,6 +2006,7 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
1950
2006
  readonly runId: string;
1951
2007
  };
1952
2008
  readonly query: {
2009
+ readonly ownerUserId?: string | undefined;
1953
2010
  readonly processId: string;
1954
2011
  readonly stream: "pty" | "stderr" | "stdout";
1955
2012
  readonly atSequence?: string | undefined;
@@ -1968,6 +2025,7 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
1968
2025
  readonly runId: string;
1969
2026
  };
1970
2027
  readonly query: {
2028
+ readonly ownerUserId?: string | undefined;
1971
2029
  readonly fromSequence?: string | undefined;
1972
2030
  readonly toSequence?: string | undefined;
1973
2031
  readonly limit?: string | undefined;
@@ -2364,6 +2422,33 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
2364
2422
  readonly status: "ok";
2365
2423
  }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError), [Mode] extends ["response-only"] ? never : never>;
2366
2424
  };
2425
+ readonly users: {
2426
+ readonly ensureUser: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
2427
+ readonly payload: {
2428
+ readonly email: string;
2429
+ readonly name: string;
2430
+ readonly userId?: string | undefined;
2431
+ };
2432
+ readonly responseMode?: Mode;
2433
+ }) => Effect.Effect<HttpApiClient.Client.Response<{
2434
+ readonly userId: string;
2435
+ readonly email: string;
2436
+ readonly name: string;
2437
+ readonly createdAt: string;
2438
+ readonly created: boolean;
2439
+ }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").UserBadRequestError | import("@sealant/api-contracts").UserInternalServerError), [Mode] extends ["response-only"] ? never : never>;
2440
+ readonly getUser: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
2441
+ readonly params: {
2442
+ readonly userId: string;
2443
+ };
2444
+ readonly responseMode?: Mode;
2445
+ }) => Effect.Effect<HttpApiClient.Client.Response<{
2446
+ readonly userId: string;
2447
+ readonly email: string;
2448
+ readonly name: string;
2449
+ readonly createdAt: string;
2450
+ }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").UserInternalServerError | import("@sealant/api-contracts").UserNotFoundError), [Mode] extends ["response-only"] ? never : never>;
2451
+ };
2367
2452
  readonly workspaces: {
2368
2453
  readonly createWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
2369
2454
  readonly headers: {
@@ -2462,6 +2547,9 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
2462
2547
  readonly params: {
2463
2548
  readonly workspaceId: string;
2464
2549
  };
2550
+ readonly query: {
2551
+ readonly ownerUserId?: string | undefined;
2552
+ };
2465
2553
  readonly responseMode?: Mode;
2466
2554
  }) => Effect.Effect<HttpApiClient.Client.Response<{
2467
2555
  readonly workspaceId: string;
@@ -37,7 +37,7 @@ const execWorkspaceEffect = (ctx, init, argv, options) => Effect.gen(function* (
37
37
  }));
38
38
  }
39
39
  yield* Effect.sleep(POLL_INTERVAL);
40
- wire = yield* getRunOp(runId);
40
+ wire = yield* getRunOp(runId, ctx.config.hostLocal.ownerUserId);
41
41
  }
42
42
  // Exec framing: "completed" means every command executed and was recorded — anything else means
43
43
  // the machinery broke and the exit code cannot be trusted, which IS the error case.
@@ -48,7 +48,7 @@ const execWorkspaceEffect = (ctx, init, argv, options) => Effect.gen(function* (
48
48
  const processId = findCommandProcessId(started, executable);
49
49
  const stdout = processId === undefined ? "" : yield* readScrollback(runId, processId, "stdout");
50
50
  const stderr = processId === undefined ? "" : yield* readScrollback(runId, processId, "stderr");
51
- const changes = toRunChangesData(yield* getRunChangesOp(runId));
51
+ const changes = toRunChangesData(yield* getRunChangesOp(runId, ctx.config.hostLocal.ownerUserId));
52
52
  return {
53
53
  exitCode: wire.exitCode ?? -1,
54
54
  stdout,
@@ -24,4 +24,4 @@ export { makeSdkRuntime } from "./runtime.js";
24
24
  export type { SdkRuntime, SdkServices } from "./runtime.js";
25
25
  export { resolveInternalConfig } from "../internal/config.js";
26
26
  export type { SealantHostLocalConfig, SealantInternalConfig } from "../internal/config.js";
27
- export { InferenceBadRequestError, InferenceConflictError, InferenceInternalServerError, InferenceNotFoundError, InferenceUnavailableError, RunBadRequestError, RunInternalServerError, RunNotFoundError, WorkspaceBadGatewayError, WorkspaceBadRequestError, WorkspaceConflictError, WorkspaceForbiddenError, WorkspaceInternalServerError, WorkspaceNotFoundError, WorkspaceServiceUnavailableError, WorkspaceUnauthorizedError, } from "@sealant/api-contracts";
27
+ export { ConnectedAccountBadRequestError, ConnectedAccountConflictError, ConnectedAccountInternalServerError, ConnectedAccountNotFoundError, ConnectedAccountServiceUnavailableError, InferenceBadRequestError, InferenceConflictError, InferenceInternalServerError, InferenceNotFoundError, InferenceUnavailableError, RunBadRequestError, RunInternalServerError, RunNotFoundError, UserBadRequestError, UserInternalServerError, UserNotFoundError, WorkspaceBadGatewayError, WorkspaceBadRequestError, WorkspaceConflictError, WorkspaceForbiddenError, WorkspaceInternalServerError, WorkspaceNotFoundError, WorkspaceServiceUnavailableError, WorkspaceUnauthorizedError, } from "@sealant/api-contracts";
@@ -28,4 +28,4 @@ export { resolveInternalConfig } from "../internal/config.js";
28
28
  // The typed contract errors carried on the client's failure channel (workspaces + runs +
29
29
  // inference — the groups the operations above call). Re-exported so Effect consumers don't need to
30
30
  // depend on the contracts package directly to `Effect.catchTag` a failure.
31
- export { InferenceBadRequestError, InferenceConflictError, InferenceInternalServerError, InferenceNotFoundError, InferenceUnavailableError, RunBadRequestError, RunInternalServerError, RunNotFoundError, WorkspaceBadGatewayError, WorkspaceBadRequestError, WorkspaceConflictError, WorkspaceForbiddenError, WorkspaceInternalServerError, WorkspaceNotFoundError, WorkspaceServiceUnavailableError, WorkspaceUnauthorizedError, } from "@sealant/api-contracts";
31
+ export { ConnectedAccountBadRequestError, ConnectedAccountConflictError, ConnectedAccountInternalServerError, ConnectedAccountNotFoundError, ConnectedAccountServiceUnavailableError, InferenceBadRequestError, InferenceConflictError, InferenceInternalServerError, InferenceNotFoundError, InferenceUnavailableError, RunBadRequestError, RunInternalServerError, RunNotFoundError, UserBadRequestError, UserInternalServerError, UserNotFoundError, WorkspaceBadGatewayError, WorkspaceBadRequestError, WorkspaceConflictError, WorkspaceForbiddenError, WorkspaceInternalServerError, WorkspaceNotFoundError, WorkspaceServiceUnavailableError, WorkspaceUnauthorizedError, } from "@sealant/api-contracts";
@@ -37,7 +37,7 @@ export declare const createWorkspaceOp: (payload: {
37
37
  readonly repository: string;
38
38
  readonly tag: string;
39
39
  }, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadGatewayError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceForbiddenError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError | import("@sealant/api-contracts").WorkspaceServiceUnavailableError, SealantApiClient>;
40
- export declare const getWorkspaceOp: (workspaceId: string) => Effect.Effect<{
40
+ export declare const getWorkspaceOp: (workspaceId: string, ownerUserId?: string | undefined) => Effect.Effect<{
41
41
  readonly workspaceId: string;
42
42
  readonly name: string;
43
43
  readonly ownerUserId: string;
@@ -194,7 +194,7 @@ export declare const createRunOp: (payload: {
194
194
  readonly createdAt: string;
195
195
  readonly updatedAt: string;
196
196
  }, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RunBadRequestError | import("@sealant/api-contracts").RunInternalServerError | import("@sealant/api-contracts").RunNotFoundError | import("effect/Schema").SchemaError, SealantApiClient>;
197
- export declare const getRunOp: (runId: string) => Effect.Effect<{
197
+ export declare const getRunOp: (runId: string, ownerUserId?: string | undefined) => Effect.Effect<{
198
198
  readonly runId: string;
199
199
  readonly workspaceId: string;
200
200
  readonly attemptId?: string | undefined;
@@ -284,6 +284,7 @@ export declare const updateRunOp: (runId: string, payload: {
284
284
  readonly updatedAt: string;
285
285
  }, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RunBadRequestError | import("@sealant/api-contracts").RunInternalServerError | import("@sealant/api-contracts").RunNotFoundError | import("effect/Schema").SchemaError, SealantApiClient>;
286
286
  export declare const getRunTimelineOp: (runId: string, query: {
287
+ readonly ownerUserId?: string | undefined;
287
288
  readonly fromSequence?: string | undefined;
288
289
  readonly toSequence?: string | undefined;
289
290
  readonly limit?: string | undefined;
@@ -300,6 +301,7 @@ export declare const getRunTimelineOp: (runId: string, query: {
300
301
  readonly confidence: number;
301
302
  }[], import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RunBadRequestError | import("@sealant/api-contracts").RunInternalServerError | import("@sealant/api-contracts").RunNotFoundError | import("effect/Schema").SchemaError, SealantApiClient>;
302
303
  export declare const getRunScrollbackOp: (runId: string, query: {
304
+ readonly ownerUserId?: string | undefined;
303
305
  readonly processId: string;
304
306
  readonly stream: "pty" | "stderr" | "stdout";
305
307
  readonly atSequence?: string | undefined;
@@ -311,7 +313,7 @@ export declare const getRunScrollbackOp: (runId: string, query: {
311
313
  readonly byteCount: number;
312
314
  readonly contentBase64: string;
313
315
  }, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RunBadRequestError | import("@sealant/api-contracts").RunInternalServerError | import("@sealant/api-contracts").RunNotFoundError | import("effect/Schema").SchemaError, SealantApiClient>;
314
- export declare const getRunLossOp: (runId: string) => Effect.Effect<{
316
+ export declare const getRunLossOp: (runId: string, ownerUserId?: string | undefined) => Effect.Effect<{
315
317
  readonly runId: string;
316
318
  readonly droppedEventCount: string;
317
319
  readonly sequenceGapCount: number;
@@ -326,7 +328,7 @@ export declare const getRunLossOp: (runId: string) => Effect.Effect<{
326
328
  readonly reason?: string | undefined;
327
329
  }[];
328
330
  }, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RunInternalServerError | import("@sealant/api-contracts").RunNotFoundError | import("effect/Schema").SchemaError, SealantApiClient>;
329
- export declare const getRunChangesOp: (runId: string) => Effect.Effect<{
331
+ export declare const getRunChangesOp: (runId: string, ownerUserId?: string | undefined) => Effect.Effect<{
330
332
  readonly files: readonly {
331
333
  readonly path: string;
332
334
  readonly change: "added" | "deleted" | "modified" | "renamed";
@@ -491,6 +493,75 @@ export declare const createAccessTokenOp: (payload: {
491
493
  readonly createdAt: string;
492
494
  readonly token: string;
493
495
  }, import("@sealant/api-contracts").AccessTokenBadRequestError | import("@sealant/api-contracts").AccessTokenInternalServerError | import("@sealant/api-contracts").AccessTokenNotFoundError | import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError, SealantApiClient>;
496
+ export declare const ensureUserOp: (payload: {
497
+ readonly email: string;
498
+ readonly name: string;
499
+ readonly userId?: string | undefined;
500
+ }) => Effect.Effect<{
501
+ readonly userId: string;
502
+ readonly email: string;
503
+ readonly name: string;
504
+ readonly createdAt: string;
505
+ readonly created: boolean;
506
+ }, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").UserBadRequestError | import("@sealant/api-contracts").UserInternalServerError, SealantApiClient>;
507
+ export declare const getUserOp: (userId: string) => Effect.Effect<{
508
+ readonly userId: string;
509
+ readonly email: string;
510
+ readonly name: string;
511
+ readonly createdAt: string;
512
+ }, import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").UserInternalServerError | import("@sealant/api-contracts").UserNotFoundError, SealantApiClient>;
513
+ export declare const listConnectedAccountsOp: (ownerUserId: string) => Effect.Effect<{
514
+ readonly items: readonly {
515
+ readonly connectedAccountId: string;
516
+ readonly ownerUserId: string;
517
+ readonly provider: "claude" | "codex" | "github";
518
+ readonly name: string;
519
+ readonly kind: string;
520
+ readonly status: "active" | "archived" | "invalid";
521
+ readonly metadata: {
522
+ readonly [x: string]: unknown;
523
+ };
524
+ readonly connectedAt: string;
525
+ readonly updatedAt: string;
526
+ readonly lastUsedAt: string | null;
527
+ readonly lastSyncedAt: string | null;
528
+ }[];
529
+ }, import("@sealant/api-contracts").ConnectedAccountInternalServerError | import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError, SealantApiClient>;
530
+ export declare const createConnectedAccountOp: (payload: {
531
+ readonly ownerUserId: string;
532
+ readonly provider: "claude" | "codex" | "github";
533
+ readonly name?: string | undefined;
534
+ readonly secret: string;
535
+ }) => Effect.Effect<{
536
+ readonly connectedAccountId: string;
537
+ readonly ownerUserId: string;
538
+ readonly provider: "claude" | "codex" | "github";
539
+ readonly name: string;
540
+ readonly kind: string;
541
+ readonly status: "active" | "archived" | "invalid";
542
+ readonly metadata: {
543
+ readonly [x: string]: unknown;
544
+ };
545
+ readonly connectedAt: string;
546
+ readonly updatedAt: string;
547
+ readonly lastUsedAt: string | null;
548
+ readonly lastSyncedAt: string | null;
549
+ }, import("@sealant/api-contracts").ConnectedAccountBadRequestError | import("@sealant/api-contracts").ConnectedAccountConflictError | import("@sealant/api-contracts").ConnectedAccountInternalServerError | import("@sealant/api-contracts").ConnectedAccountNotFoundError | import("@sealant/api-contracts").ConnectedAccountServiceUnavailableError | import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError, SealantApiClient>;
550
+ export declare const archiveConnectedAccountOp: (connectedAccountId: string, ownerUserId: string) => Effect.Effect<{
551
+ readonly connectedAccountId: string;
552
+ readonly ownerUserId: string;
553
+ readonly provider: "claude" | "codex" | "github";
554
+ readonly name: string;
555
+ readonly kind: string;
556
+ readonly status: "active" | "archived" | "invalid";
557
+ readonly metadata: {
558
+ readonly [x: string]: unknown;
559
+ };
560
+ readonly connectedAt: string;
561
+ readonly updatedAt: string;
562
+ readonly lastUsedAt: string | null;
563
+ readonly lastSyncedAt: string | null;
564
+ }, import("@sealant/api-contracts").ConnectedAccountInternalServerError | import("@sealant/api-contracts").ConnectedAccountNotFoundError | import("effect/unstable/http/HttpClientError").HttpClientError | import("effect/Schema").SchemaError, SealantApiClient>;
494
565
  export declare const inferenceRespondOp: (payload: {
495
566
  readonly ownerUserId: string;
496
567
  readonly credentials?: {
@@ -5,7 +5,10 @@ export const createWorkspaceOp = (payload, idempotencyKey) => Effect.flatMap(Sea
5
5
  payload,
6
6
  headers: idempotencyKey === undefined ? {} : { "idempotency-key": idempotencyKey },
7
7
  }));
8
- export const getWorkspaceOp = (workspaceId) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.getWorkspace({ params: { workspaceId } }));
8
+ export const getWorkspaceOp = (workspaceId, ownerUserId) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.getWorkspace({
9
+ params: { workspaceId },
10
+ query: ownerUserId === undefined ? {} : { ownerUserId },
11
+ }));
9
12
  export const listWorkspacesOp = (query) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.listWorkspaces({ query }));
10
13
  export const execWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.execWorkspace({ params: { workspaceId }, payload }));
11
14
  export const stopWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.stopWorkspace({ params: { workspaceId }, payload }));
@@ -13,13 +16,14 @@ export const restartWorkspaceOp = (workspaceId, payload) => Effect.flatMap(Seala
13
16
  export const expireWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.expireWorkspace({ params: { workspaceId }, payload }));
14
17
  // ---- runs ----
15
18
  export const createRunOp = (payload) => Effect.flatMap(SealantApiClient, (client) => client.runs.createRun({ payload }));
16
- export const getRunOp = (runId) => Effect.flatMap(SealantApiClient, (client) => client.runs.getRun({ params: { runId } }));
19
+ const ownerQuery = (ownerUserId) => ownerUserId === undefined ? {} : { ownerUserId };
20
+ export const getRunOp = (runId, ownerUserId) => Effect.flatMap(SealantApiClient, (client) => client.runs.getRun({ params: { runId }, query: ownerQuery(ownerUserId) }));
17
21
  export const listRunsOp = (query) => Effect.flatMap(SealantApiClient, (client) => client.runs.listRuns({ query }));
18
22
  export const updateRunOp = (runId, payload) => Effect.flatMap(SealantApiClient, (client) => client.runs.updateRun({ params: { runId }, payload }));
19
23
  export const getRunTimelineOp = (runId, query) => Effect.flatMap(SealantApiClient, (client) => Effect.map(client.runs.getRunTimeline({ params: { runId }, query }), (r) => r.items));
20
24
  export const getRunScrollbackOp = (runId, query) => Effect.flatMap(SealantApiClient, (client) => client.runs.getRunScrollback({ params: { runId }, query }));
21
- export const getRunLossOp = (runId) => Effect.flatMap(SealantApiClient, (client) => client.runs.getRunLoss({ params: { runId } }));
22
- export const getRunChangesOp = (runId) => Effect.flatMap(SealantApiClient, (client) => client.runs.getRunChanges({ params: { runId } }));
25
+ export const getRunLossOp = (runId, ownerUserId) => Effect.flatMap(SealantApiClient, (client) => client.runs.getRunLoss({ params: { runId }, query: ownerQuery(ownerUserId) }));
26
+ export const getRunChangesOp = (runId, ownerUserId) => Effect.flatMap(SealantApiClient, (client) => client.runs.getRunChanges({ params: { runId }, query: ownerQuery(ownerUserId) }));
23
27
  // ---- sessions ----
24
28
  export const createSessionOp = (payload) => Effect.flatMap(SealantApiClient, (client) => client.sessions.createSession({ payload, headers: {} }));
25
29
  export const getSessionOp = (sessionId, ownerUserId) => Effect.flatMap(SealantApiClient, (client) => client.sessions.getSession({
@@ -35,5 +39,15 @@ export const signalSessionOp = (sessionId, payload) => Effect.flatMap(SealantApi
35
39
  export const closeSessionOp = (sessionId, payload) => Effect.flatMap(SealantApiClient, (client) => client.sessions.closeSession({ params: { sessionId }, headers: {}, payload }));
36
40
  // ---- access tokens ----
37
41
  export const createAccessTokenOp = (payload) => Effect.flatMap(SealantApiClient, (client) => client.accessTokens.createAccessToken({ payload }));
42
+ // ---- users ----
43
+ export const ensureUserOp = (payload) => Effect.flatMap(SealantApiClient, (client) => client.users.ensureUser({ payload }));
44
+ export const getUserOp = (userId) => Effect.flatMap(SealantApiClient, (client) => client.users.getUser({ params: { userId } }));
45
+ // ---- connected accounts ----
46
+ export const listConnectedAccountsOp = (ownerUserId) => Effect.flatMap(SealantApiClient, (client) => client.connectedAccounts.listConnectedAccounts({ query: { ownerUserId } }));
47
+ export const createConnectedAccountOp = (payload) => Effect.flatMap(SealantApiClient, (client) => client.connectedAccounts.createConnectedAccount({ payload }));
48
+ export const archiveConnectedAccountOp = (connectedAccountId, ownerUserId) => Effect.flatMap(SealantApiClient, (client) => client.connectedAccounts.archiveConnectedAccount({
49
+ params: { connectedAccountId },
50
+ query: { ownerUserId },
51
+ }));
38
52
  // ---- inference ----
39
53
  export const inferenceRespondOp = (payload) => Effect.flatMap(SealantApiClient, (client) => client.inference.respond({ payload }));
@@ -38,7 +38,7 @@ const createHarnessRunEffect = (ctx, init, prompt, options) => Effect.gen(functi
38
38
  ...metadata,
39
39
  });
40
40
  }
41
- const details = yield* getWorkspaceOp(init.id);
41
+ const details = yield* getWorkspaceOp(init.id, ctx.config.hostLocal.ownerUserId);
42
42
  const spec = details.spec;
43
43
  const harnessId = spec?.harness?.id;
44
44
  if (harnessId === undefined) {
@@ -66,10 +66,10 @@ const runHarnessEffect = (ctx, init, prompt, options) => Effect.gen(function* ()
66
66
  }));
67
67
  }
68
68
  yield* Effect.sleep(POLL_INTERVAL);
69
- wire = yield* getRunOp(runId);
69
+ wire = yield* getRunOp(runId, ctx.config.hostLocal.ownerUserId);
70
70
  }
71
71
  // Read the changes the run produced (captured server-side).
72
- const changes = toRunChangesData(yield* getRunChangesOp(runId));
72
+ const changes = toRunChangesData(yield* getRunChangesOp(runId, ctx.config.hostLocal.ownerUserId));
73
73
  return makeRun(ctx, { wire, changes });
74
74
  });
75
75
  /** The BLOCKING `harness.run()` implementation, registered into the Workspace facade by the client. */
@@ -135,7 +135,10 @@ export const renderTranscript = (commands) => {
135
135
  return `${blocks.join("\n\n")}\n`;
136
136
  };
137
137
  export const makeRunRecord = (ctx, runId) => {
138
- const fetchTimeline = (from) => ctx.runtime.run(getRunTimelineOp(runId, from === undefined ? {} : { fromSequence: from.toString() }));
138
+ const fetchTimeline = (from) => ctx.runtime.run(getRunTimelineOp(runId, {
139
+ ownerUserId: ctx.config.hostLocal.ownerUserId,
140
+ ...(from === undefined ? {} : { fromSequence: from.toString() }),
141
+ }));
139
142
  return {
140
143
  runId,
141
144
  replay: async (options) => {
@@ -171,7 +174,10 @@ export const makeRunRecord = (ctx, runId) => {
171
174
  let from = options?.from;
172
175
  const deadline = Date.now() + STREAM_TIMEOUT_MS;
173
176
  for (;;) {
174
- const wire = await ctxRun.run(getRunTimelineOp(runId, from === undefined ? {} : { fromSequence: from.toString() }));
177
+ const wire = await ctxRun.run(getRunTimelineOp(runId, {
178
+ ownerUserId: ctx.config.hostLocal.ownerUserId,
179
+ ...(from === undefined ? {} : { fromSequence: from.toString() }),
180
+ }));
175
181
  for (const entry of wire) {
176
182
  const mapped = toTimelineEntry(entry);
177
183
  yield mapped;
@@ -179,9 +185,12 @@ export const makeRunRecord = (ctx, runId) => {
179
185
  }
180
186
  // Stop once the run is terminal — with one final drain to catch entries written between the
181
187
  // last timeline fetch and the status check.
182
- const run = await ctxRun.run(getRunOp(runId));
188
+ const run = await ctxRun.run(getRunOp(runId, ctx.config.hostLocal.ownerUserId));
183
189
  if (TERMINAL_RUN_STATUSES.has(run.status)) {
184
- const tail = await ctxRun.run(getRunTimelineOp(runId, from === undefined ? {} : { fromSequence: from.toString() }));
190
+ const tail = await ctxRun.run(getRunTimelineOp(runId, {
191
+ ownerUserId: ctx.config.hostLocal.ownerUserId,
192
+ ...(from === undefined ? {} : { fromSequence: from.toString() }),
193
+ }));
185
194
  for (const entry of tail) {
186
195
  yield toTimelineEntry(entry);
187
196
  }
@@ -208,7 +217,11 @@ export const makeRunRecord = (ctx, runId) => {
208
217
  scrollback: (processId, stream) => {
209
218
  const run = ctx.runtime;
210
219
  async function* iterate() {
211
- const response = await run.run(getRunScrollbackOp(runId, { processId, stream }));
220
+ const response = await run.run(getRunScrollbackOp(runId, {
221
+ ownerUserId: ctx.config.hostLocal.ownerUserId,
222
+ processId,
223
+ stream,
224
+ }));
212
225
  const bytes = Buffer.from(response.contentBase64, "base64");
213
226
  if (bytes.byteLength > 0) {
214
227
  yield new Uint8Array(bytes.buffer, bytes.byteOffset, bytes.byteLength);
@@ -216,9 +229,9 @@ export const makeRunRecord = (ctx, runId) => {
216
229
  }
217
230
  return iterate();
218
231
  },
219
- loss: async () => toLossReport(await ctx.runtime.run(getRunLossOp(runId))),
232
+ loss: async () => toLossReport(await ctx.runtime.run(getRunLossOp(runId, ctx.config.hostLocal.ownerUserId))),
220
233
  summary: async () => {
221
- const run = await ctx.runtime.run(getRunOp(runId));
234
+ const run = await ctx.runtime.run(getRunOp(runId, ctx.config.hostLocal.ownerUserId));
222
235
  const timeline = await fetchTimeline();
223
236
  const durationMs = run.startedAt !== undefined && run.finishedAt !== undefined
224
237
  ? new Date(run.finishedAt).getTime() - new Date(run.startedAt).getTime()
@@ -46,11 +46,12 @@ export const makeRun = (ctx, init) => {
46
46
  });
47
47
  }
48
48
  await delay(WAIT_POLL_INTERVAL_MS);
49
- current = await ctx.runtime.run(getRunOp(runId));
49
+ current = await ctx.runtime.run(getRunOp(runId, ctx.config.hostLocal.ownerUserId));
50
50
  }
51
51
  // Settle the changes: a handle from `harness.start()` or `runs.get()` has none captured yet,
52
52
  // so read the server-side capture now that the run is terminal.
53
- const settledChanges = changesData ?? toRunChangesData(await ctx.runtime.run(getRunChangesOp(runId)));
53
+ const settledChanges = changesData ??
54
+ toRunChangesData(await ctx.runtime.run(getRunChangesOp(runId, ctx.config.hostLocal.ownerUserId)));
54
55
  return makeRun(ctx, { wire: current, changes: settledChanges });
55
56
  },
56
57
  };
@@ -3,18 +3,19 @@ import { closeSessionOp, getSessionOp, getSessionOutputOp, resizeSessionOp, send
3
3
  * Open the held-WebSocket terminal attachment (the data plane). One socket:
4
4
  * binary frames are PTY bytes in both directions, text frames are control
5
5
  * JSON (`{"t":"resize",...}` up, `{"t":"end"}` down). Auth rides the connect —
6
- * `?token=` for apiKey clients (WebSocket cannot set headers), `?ownerUserId=`
7
- * for host-local and never repeats per event.
6
+ * `?ownerUserId=` always, plus `?token=` for apiKey clients (WebSocket cannot
7
+ * set headers; a service principal needs the owner assertion *and* its key)
8
+ * and never repeats per event.
8
9
  */
9
10
  const openAttachment = (ctx, sessionId, options) => {
10
11
  const config = ctx.config;
11
12
  const url = new URL(`/v1/sessions/${sessionId}/attach`, config.baseUrl);
12
13
  url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
13
14
  url.searchParams.set("from", (options?.from ?? 0n).toString());
14
- if (config.apiKey === undefined) {
15
- url.searchParams.set("ownerUserId", config.hostLocal.ownerUserId);
16
- }
17
- else {
15
+ // The owner assertion always rides the URL; a service principal needs it *alongside* its key,
16
+ // and WebSocket cannot carry headers, so the key rides the URL too.
17
+ url.searchParams.set("ownerUserId", config.hostLocal.ownerUserId);
18
+ if (config.apiKey !== undefined) {
18
19
  url.searchParams.set("token", config.apiKey);
19
20
  }
20
21
  return new Promise((resolve, reject) => {
@@ -126,9 +127,7 @@ async function* streamOverSse(ctx, sessionId, from, signal) {
126
127
  const fetchImpl = config.fetch ?? fetch;
127
128
  const url = new URL(`/v1/sessions/${sessionId}/output/stream`, config.baseUrl);
128
129
  url.searchParams.set("from", from.toString());
129
- if (config.apiKey === undefined) {
130
- url.searchParams.set("ownerUserId", config.hostLocal.ownerUserId);
131
- }
130
+ url.searchParams.set("ownerUserId", config.hostLocal.ownerUserId);
132
131
  const response = await fetchImpl(url, {
133
132
  headers: {
134
133
  accept: "text/event-stream",
@@ -63,7 +63,7 @@ export const makeWorkspace = (ctx, init) => {
63
63
  if (init.harness !== undefined) {
64
64
  return [init.harness.launchCommand ?? init.harness.id];
65
65
  }
66
- const details = await ctx.runtime.run(getWorkspaceOp(init.id));
66
+ const details = await ctx.runtime.run(getWorkspaceOp(init.id, ctx.config.hostLocal.ownerUserId));
67
67
  const spec = details.spec;
68
68
  const harnessId = spec?.harness?.id;
69
69
  if (harnessId === undefined) {
@@ -93,13 +93,13 @@ export const makeWorkspace = (ctx, init) => {
93
93
  id: init.id,
94
94
  name: init.name,
95
95
  status: async () => {
96
- const details = await ctx.runtime.run(getWorkspaceOp(init.id));
96
+ const details = await ctx.runtime.run(getWorkspaceOp(init.id, ctx.config.hostLocal.ownerUserId));
97
97
  return details.status;
98
98
  },
99
99
  ready: async () => {
100
100
  const deadline = Date.now() + READY_TIMEOUT_MS;
101
101
  for (;;) {
102
- const details = await ctx.runtime.run(getWorkspaceOp(init.id));
102
+ const details = await ctx.runtime.run(getWorkspaceOp(init.id, ctx.config.hostLocal.ownerUserId));
103
103
  // Gate on the coarse "ready" status, which the control plane now emits ONLY after the
104
104
  // in-workspace daemon's control socket is accepting (readiness probe in the launch path).
105
105
  // This is honest: when ready() resolves, harness.run() can connect without racing the socket.
@@ -128,7 +128,7 @@ export const makeWorkspace = (ctx, init) => {
128
128
  let lastStatus;
129
129
  const deadline = Date.now() + READY_TIMEOUT_MS;
130
130
  for (;;) {
131
- const details = await ctxRun.run(getWorkspaceOp(init.id));
131
+ const details = await ctxRun.run(getWorkspaceOp(init.id, ctx.config.hostLocal.ownerUserId));
132
132
  if (details.status !== lastStatus) {
133
133
  lastStatus = details.status;
134
134
  yield {
@@ -156,7 +156,7 @@ export const makeWorkspace = (ctx, init) => {
156
156
  await ctx.runtime.run(stopWorkspaceOp(init.id, { ownerUserId }));
157
157
  const deadline = Date.now() + STOP_TIMEOUT_MS;
158
158
  for (;;) {
159
- const details = await ctx.runtime.run(getWorkspaceOp(init.id));
159
+ const details = await ctx.runtime.run(getWorkspaceOp(init.id, ctx.config.hostLocal.ownerUserId));
160
160
  if (details.status === "stopped") {
161
161
  return;
162
162
  }
@@ -214,10 +214,10 @@ const openForward = (ctx, workspaceId, port, options) => {
214
214
  if (options?.protocol === "udp") {
215
215
  url.searchParams.set("protocol", "udp");
216
216
  }
217
- if (config.apiKey === undefined) {
218
- url.searchParams.set("ownerUserId", config.hostLocal.ownerUserId);
219
- }
220
- else {
217
+ // The owner assertion always rides the URL (a service principal needs it alongside its
218
+ // key, and WebSocket cannot carry headers — same contract as the session attach).
219
+ url.searchParams.set("ownerUserId", config.hostLocal.ownerUserId);
220
+ if (config.apiKey !== undefined) {
221
221
  url.searchParams.set("token", config.apiKey);
222
222
  }
223
223
  return new Promise((resolve, reject) => {
@@ -4,7 +4,8 @@
4
4
  * no contract change. The public `repository` is the SOURCE git repo (it becomes
5
5
  * `spec.sources.workspace.url`); the contract's `repository`/`tag` are the OCI push coordinates, which
6
6
  * we derive. `customization.enableSealantd` is forced on (it bakes + launches the daemon the run path
7
- * connects to), the runtime target is pinned to docker (the only bridgeable adapter today), and the
7
+ * connects to), the runtime target is `auto` (the deployment's default adapter — Docker on self-host, Kubernetes
8
+ * when the worker is configured for a cluster), and the
8
9
  * foreground is a keepalive so the workspace idles with the daemon up and the harness is exec'd on
9
10
  * demand by `run()` rather than launched at boot. `options.credentials`, if present, is lowered via
10
11
  * `mapWorkspaceCredentials` (see `./credentials.js`) and folded into `spec.credentials`; the control
@@ -182,7 +183,9 @@ export const buildCreateWorkspaceRequest = (options, config) => {
182
183
  os: options.baseImage !== undefined
183
184
  ? { family: "custom", mode: "require", baseImage: options.baseImage }
184
185
  : { family: options.os ?? "fedora", mode: "prefer" },
185
- runtime: { family: "docker", mode: "require" },
186
+ // `auto` = the deployment's DEFAULT_RUNTIME_ADAPTER. SDK callers don't know (and must not
187
+ // care) whether the control plane runs workspaces as containers or Pods.
188
+ runtime: { family: "auto", mode: "prefer" },
186
189
  },
187
190
  lifecycle: {
188
191
  startup: { foreground: { kind: "command", run: "sleep infinity", shell: "bash" } },
@@ -3,9 +3,9 @@
3
3
  *
4
4
  * The PUBLIC surface (`SealantConfig` in `../types.ts`) is intentionally minimal: `{ baseUrl, apiKey }`.
5
5
  * The SDK is now a thin HTTP client (run execution + telemetry moved server-side), so the only
6
- * host-local concerns left are a pre-auth owner principal and the registry id used on create/run
7
- * payloads. These live HERE resolved from the environment with docker-compose defaults so they
8
- * never leak into the published `SealantConfig`, and they disappear entirely once auth lands.
6
+ * host-local concerns left are the owner principal and the registry id used on create/run
7
+ * payloads. The owner comes from `SealantConfig.ownerUserId` when a product acts on behalf of a
8
+ * specific user (one client per user), else from the environment with docker-compose defaults.
9
9
  */
10
10
  import type { SealantConfig } from "../types.js";
11
11
  export interface SealantHostLocalConfig {
@@ -11,7 +11,7 @@ export const resolveInternalConfig = (config) => ({
11
11
  apiKey: config.apiKey,
12
12
  fetch: config.fetch,
13
13
  hostLocal: {
14
- ownerUserId: env("SEALANT_OWNER_USER_ID") ?? DEFAULT_OWNER_USER_ID,
14
+ ownerUserId: config.ownerUserId ?? env("SEALANT_OWNER_USER_ID") ?? DEFAULT_OWNER_USER_ID,
15
15
  registryId: env("SEALANT_REGISTRY_ID") ?? DEFAULT_REGISTRY_ID,
16
16
  },
17
17
  });
package/dist/types.d.ts CHANGED
@@ -23,8 +23,18 @@
23
23
  export interface SealantConfig {
24
24
  /** Base URL of the Sealant control-plane API (e.g. `http://localhost:8080`). */
25
25
  readonly baseUrl: string;
26
- /** Bearer token for authenticated deployments. Optional for a localhost demo with no auth. */
26
+ /**
27
+ * Bearer secret for authenticated deployments: a SERVICE KEY (`SEALANT_SERVICE_KEYS` on the
28
+ * control plane — lets this client act on behalf of any `ownerUserId`) or a scoped user access
29
+ * token (session surface only). Optional for a localhost demo with no auth.
30
+ */
27
31
  readonly apiKey?: string;
32
+ /**
33
+ * The user every call is attributed to. A product that owns its own login builds ONE client per
34
+ * user with that user's Sealant id (see `users.ensure`). Defaults to `SEALANT_OWNER_USER_ID`,
35
+ * then `usr_local`.
36
+ */
37
+ readonly ownerUserId?: string;
28
38
  /** Override the `fetch` implementation (tests, custom agents, proxies). */
29
39
  readonly fetch?: typeof fetch;
30
40
  }
@@ -893,3 +903,65 @@ export interface InferenceContinueOptions {
893
903
  export interface InferenceNamespace {
894
904
  respond(options: InferenceRespondOptions | InferenceContinueOptions): Promise<InferenceResponse>;
895
905
  }
906
+ export interface SealantUser {
907
+ readonly userId: string;
908
+ readonly email: string;
909
+ readonly name: string;
910
+ readonly createdAt: string;
911
+ }
912
+ export interface EnsureUserOptions {
913
+ readonly email: string;
914
+ readonly name: string;
915
+ /** Caller-chosen id for a NEW user; ignored when the email already exists. */
916
+ readonly userId?: string;
917
+ }
918
+ export interface EnsuredUser extends SealantUser {
919
+ /** True when this call created the user. */
920
+ readonly created: boolean;
921
+ }
922
+ /**
923
+ * Identity rows for products that own their own login. `ensure` is idempotent on email: call it on
924
+ * every sign-in and build the per-user client with the returned `userId` as `ownerUserId`.
925
+ */
926
+ export interface UsersNamespace {
927
+ ensure(options: EnsureUserOptions): Promise<EnsuredUser>;
928
+ get(userId: string): Promise<SealantUser>;
929
+ }
930
+ export type ConnectedAccountProvider = "claude" | "codex" | "github";
931
+ export type ConnectedAccountStatus = "active" | "invalid" | "archived";
932
+ /** A connected account as every surface sees it — NEVER carries secret material. */
933
+ export interface ConnectedAccount {
934
+ readonly connectedAccountId: string;
935
+ readonly ownerUserId: string;
936
+ readonly provider: ConnectedAccountProvider;
937
+ readonly name: string;
938
+ /** Provider-shaped payload kind: oauth-token | credentials-json | auth-json | gh-cli-token. */
939
+ readonly kind: string;
940
+ readonly status: ConnectedAccountStatus;
941
+ /** Non-secret display data (token suffix, codex account email, github login + scopes). */
942
+ readonly metadata: Readonly<Record<string, unknown>>;
943
+ readonly connectedAt: string;
944
+ readonly updatedAt: string;
945
+ readonly lastUsedAt: string | null;
946
+ readonly lastSyncedAt: string | null;
947
+ }
948
+ export interface ConnectConnectedAccountOptions {
949
+ readonly provider: ConnectedAccountProvider;
950
+ /**
951
+ * Provider-shaped plaintext, passed through and sealed server-side: a Claude setup token or
952
+ * verbatim `.credentials.json`, verbatim Codex `auth.json`, or a GitHub token. Never logged.
953
+ */
954
+ readonly secret: string;
955
+ /** Account name under the provider; defaults to `default` (the one `credentials: { x: true }` picks). */
956
+ readonly name?: string;
957
+ }
958
+ /**
959
+ * The owner's connected provider accounts. `connect` upserts on (provider, name), so reconnecting
960
+ * swaps the sealed credential in place. Secrets flow one way — in; no call returns them.
961
+ */
962
+ export interface ConnectedAccountsNamespace {
963
+ list(): Promise<readonly ConnectedAccount[]>;
964
+ connect(options: ConnectConnectedAccountOptions): Promise<ConnectedAccount>;
965
+ /** Soft-archives the account; uniform not-found for "does not exist" and "not yours". */
966
+ disconnect(connectedAccountId: string): Promise<ConnectedAccount>;
967
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealant/sdk",
3
- "version": "0.21.0",
3
+ "version": "0.23.0",
4
4
  "description": "The fluent public SDK for Sealant — create a workspace, run a harness, replay the record.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -26,7 +26,7 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@sealant/api-contracts": "^0.21.0"
29
+ "@sealant/api-contracts": "^0.23.0"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@effect/vitest": "4.0.0-beta.85",