@sealant/sdk-next 0.39.0-next.712 → 0.39.0-next.714

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/README.md CHANGED
@@ -96,9 +96,16 @@ if (
96
96
  - `workspace.processUser()` reads `supported`, `unsupported` or `unknown` from the image's sealantd;
97
97
  `launch.processUser` has it after `ready()`. The call itself asks the running daemon, whose answer
98
98
  decides.
99
+ - `users.bindPerson(userId, { id, uid, home })` binds a user's person once; a different binding, or
100
+ a person id or uid another user holds, rejects (`409`).
101
+ `create({ credentialsHome, sshAsOwner: true })` runs the workspace's SSH sessions, VS Code
102
+ Remote-SSH included, as the owner's bound person: the create's owner map and `credentialsHome`
103
+ must agree with the binding (`403` otherwise). No caller names the user. `workspace.sshAsRoot()`
104
+ sets them back to root. A create with it is refused (`ssh-user-unsupported`) with nothing sent
105
+ when the control plane does not report `workspaceSshUser`.
99
106
  - `sealant.features()` reports what the control plane can do: `processUserRoutes`, `dotfilesApply`,
100
- `credentialsPartialPut`, `credentialsPiOpencode` and `captureOwnerMap`. Detect them instead of
101
- reading the control plane's version.
107
+ `credentialsPartialPut`, `credentialsPiOpencode`, `captureOwnerMap` and `workspaceSshUser`. Detect
108
+ them instead of reading the control plane's version.
102
109
 
103
110
  ## Typed record events
104
111
 
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { archiveConnectedAccountOp, archiveSshKeyOp, cancelWorkspaceCreateOp, createAccessTokenOp, createConnectedAccountOp, createSshKeyOp, createWorkspaceOp, ensureUserOp, getRunOp, getSetupStateOp, getUserOp, getWorkspaceCreateOp, getWorkspaceOp, inferenceRespondOp, inspectWorkspaceImageOp, listConnectedAccountsOp, listSshKeysOp, listWorkspacesOp, } from "./effect/operations.js";
1
+ import { archiveConnectedAccountOp, archiveSshKeyOp, cancelWorkspaceCreateOp, createAccessTokenOp, createConnectedAccountOp, createSshKeyOp, createWorkspaceOp, ensureUserOp, getRunOp, getSetupStateOp, getUserOp, bindUserPersonOp, getWorkspaceCreateOp, getWorkspaceOp, inferenceRespondOp, inspectWorkspaceImageOp, listConnectedAccountsOp, listSshKeysOp, listWorkspacesOp, } from "./effect/operations.js";
2
2
  import { runHarness, startHarness } from "./effect/run-harness.js";
3
3
  import { makeSdkRuntime } from "./effect/runtime.js";
4
4
  import { SealantError } from "./errors.js";
@@ -10,6 +10,7 @@ import { parseTtlSeconds } from "./internal/duration.js";
10
10
  import { readFeatures } from "./internal/features.js";
11
11
  import { imageSpecKey } from "./internal/image-key.js";
12
12
  import { buildInferenceRespondRequest, mapInferenceResponse } from "./internal/inference.js";
13
+ import { requireSshUser } from "./internal/process-user.js";
13
14
  import { mapSshKey, mapWorkspaceSshInfo } from "./internal/ssh.js";
14
15
  /** An idempotent create's state as the SDK reports it (an unknown wire state fails closed). */
15
16
  const toCreateState = (wire) => ({
@@ -63,6 +64,9 @@ export class Sealant {
63
64
  /** Workspace lifecycle: create, fetch, and list live environments. */
64
65
  workspaces = {
65
66
  create: async (options) => {
67
+ if (options.sshAsOwner === true) {
68
+ await requireSshUser(this.#ctx);
69
+ }
66
70
  const { payload } = buildCreateWorkspaceRequest(options, this.#ctx.config);
67
71
  const created = await this.#runtime.run(createWorkspaceOp(payload));
68
72
  const workspace = makeWorkspace(this.#ctx, {
@@ -227,6 +231,10 @@ export class Sealant {
227
231
  const wire = await this.#runtime.run(getUserOp(userId));
228
232
  return { userId: wire.userId, email: wire.email, name: wire.name, createdAt: wire.createdAt };
229
233
  },
234
+ bindPerson: async (userId, person) => {
235
+ const wire = await this.#runtime.run(bindUserPersonOp(userId, { id: person.id, uid: person.uid, home: person.home }));
236
+ return { userId: wire.userId, person: wire.person, created: wire.created };
237
+ },
230
238
  };
231
239
  /** This client's owner's Claude / Codex / GitHub accounts. Secrets go in; none come out. */
232
240
  connectedAccounts = {
@@ -1147,6 +1147,8 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
1147
1147
  readonly credentialsPartialPut?: boolean | undefined;
1148
1148
  readonly credentialsPiOpencode?: boolean | undefined;
1149
1149
  readonly captureOwnerMap?: boolean | undefined;
1150
+ readonly workspaceSshUser?: boolean | undefined;
1151
+ readonly personBinding?: boolean | undefined;
1150
1152
  } | undefined;
1151
1153
  }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError), [Mode] extends ["response-only"] ? never : never>;
1152
1154
  readonly getSetupState: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: void | {
@@ -1171,6 +1173,25 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
1171
1173
  }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError), [Mode] extends ["response-only"] ? never : never>;
1172
1174
  };
1173
1175
  readonly users: {
1176
+ readonly bindUserPerson: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
1177
+ readonly params: {
1178
+ readonly userId: string;
1179
+ };
1180
+ readonly payload: {
1181
+ readonly id: string;
1182
+ readonly uid: number;
1183
+ readonly home: string;
1184
+ };
1185
+ readonly responseMode?: Mode;
1186
+ }) => Effect.Effect<HttpApiClient.Client.Response<{
1187
+ readonly userId: string;
1188
+ readonly person: {
1189
+ readonly id: string;
1190
+ readonly uid: number;
1191
+ readonly home: string;
1192
+ };
1193
+ readonly created: boolean;
1194
+ }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").UserBadRequestError | import("@sealant/api-contracts").UserInternalServerError | import("@sealant/api-contracts").UserNotFoundError | import("@sealant/api-contracts").UserPersonConflictError), [Mode] extends ["response-only"] ? never : never>;
1174
1195
  readonly ensureUser: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
1175
1196
  readonly payload: {
1176
1197
  readonly email: string;
@@ -1282,6 +1303,18 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
1282
1303
  readonly runId?: string | undefined;
1283
1304
  readonly launchId?: string | undefined;
1284
1305
  }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceInternalServerError), [Mode] extends ["response-only"] ? never : never>;
1306
+ readonly clearWorkspaceSshUser: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
1307
+ readonly params: {
1308
+ readonly workspaceId: string;
1309
+ };
1310
+ readonly query: {
1311
+ readonly ownerUserId?: string | undefined;
1312
+ };
1313
+ readonly responseMode?: Mode;
1314
+ }) => Effect.Effect<HttpApiClient.Client.Response<{
1315
+ readonly workspaceId: string;
1316
+ readonly sshUser: null;
1317
+ }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError), [Mode] extends ["response-only"] ? never : never>;
1285
1318
  readonly createWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
1286
1319
  readonly headers: {
1287
1320
  readonly "idempotency-key"?: string | undefined;
@@ -1318,6 +1351,7 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
1318
1351
  readonly ttlSeconds?: number | undefined;
1319
1352
  readonly idempotencyKey?: string | undefined;
1320
1353
  readonly launchId?: string | undefined;
1354
+ readonly sshAsOwner?: boolean | undefined;
1321
1355
  };
1322
1356
  readonly responseMode?: Mode;
1323
1357
  }) => Effect.Effect<HttpApiClient.Client.Response<{
@@ -1340,7 +1374,7 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
1340
1374
  } | undefined;
1341
1375
  readonly replayed?: boolean | undefined;
1342
1376
  readonly launchId?: string | undefined;
1343
- }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("@sealant/api-contracts").BudgetExceededError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadGatewayError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceDockerServiceUnsupportedError | import("@sealant/api-contracts").WorkspaceForbiddenError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError | import("@sealant/api-contracts").WorkspaceRuntimeEnvReferencesUnsupportedError | import("@sealant/api-contracts").WorkspaceServiceUnavailableError), [Mode] extends ["response-only"] ? never : never>;
1377
+ }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("@sealant/api-contracts").BudgetExceededError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadGatewayError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceDockerServiceUnsupportedError | import("@sealant/api-contracts").WorkspaceForbiddenError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError | import("@sealant/api-contracts").WorkspaceRuntimeEnvReferencesUnsupportedError | import("@sealant/api-contracts").WorkspaceServiceUnavailableError | import("@sealant/api-contracts").WorkspaceSshOwnerRefusedError), [Mode] extends ["response-only"] ? never : never>;
1344
1378
  readonly execWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
1345
1379
  readonly params: {
1346
1380
  readonly workspaceId: string;
@@ -1683,6 +1717,7 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
1683
1717
  readonly headers: {
1684
1718
  readonly "x-sealant-gateway-token"?: string | undefined;
1685
1719
  readonly "x-sealant-principal-id"?: string | undefined;
1720
+ readonly "x-sealant-gateway-ssh-user"?: string | undefined;
1686
1721
  };
1687
1722
  readonly responseMode?: Mode;
1688
1723
  }) => Effect.Effect<HttpApiClient.Client.Response<{
@@ -1695,6 +1730,7 @@ declare const buildControlPlaneClient: (config: SealantInternalConfig) => Effect
1695
1730
  readonly status: "failed" | "pending" | "ready" | "running" | "stopped";
1696
1731
  readonly endpoint: string;
1697
1732
  };
1733
+ readonly sessionUser: string | null;
1698
1734
  }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError | import("@sealant/api-contracts").WorkspaceServiceUnavailableError | import("@sealant/api-contracts").WorkspaceUnauthorizedError), [Mode] extends ["response-only"] ? never : never>;
1699
1735
  readonly inspectWorkspaceImage: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
1700
1736
  readonly payload: {
@@ -3213,6 +3249,8 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
3213
3249
  readonly credentialsPartialPut?: boolean | undefined;
3214
3250
  readonly credentialsPiOpencode?: boolean | undefined;
3215
3251
  readonly captureOwnerMap?: boolean | undefined;
3252
+ readonly workspaceSshUser?: boolean | undefined;
3253
+ readonly personBinding?: boolean | undefined;
3216
3254
  } | undefined;
3217
3255
  }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError), [Mode] extends ["response-only"] ? never : never>;
3218
3256
  readonly getSetupState: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: void | {
@@ -3237,6 +3275,25 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
3237
3275
  }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError), [Mode] extends ["response-only"] ? never : never>;
3238
3276
  };
3239
3277
  readonly users: {
3278
+ readonly bindUserPerson: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
3279
+ readonly params: {
3280
+ readonly userId: string;
3281
+ };
3282
+ readonly payload: {
3283
+ readonly id: string;
3284
+ readonly uid: number;
3285
+ readonly home: string;
3286
+ };
3287
+ readonly responseMode?: Mode;
3288
+ }) => Effect.Effect<HttpApiClient.Client.Response<{
3289
+ readonly userId: string;
3290
+ readonly person: {
3291
+ readonly id: string;
3292
+ readonly uid: number;
3293
+ readonly home: string;
3294
+ };
3295
+ readonly created: boolean;
3296
+ }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").UserBadRequestError | import("@sealant/api-contracts").UserInternalServerError | import("@sealant/api-contracts").UserNotFoundError | import("@sealant/api-contracts").UserPersonConflictError), [Mode] extends ["response-only"] ? never : never>;
3240
3297
  readonly ensureUser: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
3241
3298
  readonly payload: {
3242
3299
  readonly email: string;
@@ -3348,6 +3405,18 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
3348
3405
  readonly runId?: string | undefined;
3349
3406
  readonly launchId?: string | undefined;
3350
3407
  }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceInternalServerError), [Mode] extends ["response-only"] ? never : never>;
3408
+ readonly clearWorkspaceSshUser: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
3409
+ readonly params: {
3410
+ readonly workspaceId: string;
3411
+ };
3412
+ readonly query: {
3413
+ readonly ownerUserId?: string | undefined;
3414
+ };
3415
+ readonly responseMode?: Mode;
3416
+ }) => Effect.Effect<HttpApiClient.Client.Response<{
3417
+ readonly workspaceId: string;
3418
+ readonly sshUser: null;
3419
+ }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError), [Mode] extends ["response-only"] ? never : never>;
3351
3420
  readonly createWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
3352
3421
  readonly headers: {
3353
3422
  readonly "idempotency-key"?: string | undefined;
@@ -3384,6 +3453,7 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
3384
3453
  readonly ttlSeconds?: number | undefined;
3385
3454
  readonly idempotencyKey?: string | undefined;
3386
3455
  readonly launchId?: string | undefined;
3456
+ readonly sshAsOwner?: boolean | undefined;
3387
3457
  };
3388
3458
  readonly responseMode?: Mode;
3389
3459
  }) => Effect.Effect<HttpApiClient.Client.Response<{
@@ -3406,7 +3476,7 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
3406
3476
  } | undefined;
3407
3477
  readonly replayed?: boolean | undefined;
3408
3478
  readonly launchId?: string | undefined;
3409
- }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("@sealant/api-contracts").BudgetExceededError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadGatewayError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceDockerServiceUnsupportedError | import("@sealant/api-contracts").WorkspaceForbiddenError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError | import("@sealant/api-contracts").WorkspaceRuntimeEnvReferencesUnsupportedError | import("@sealant/api-contracts").WorkspaceServiceUnavailableError), [Mode] extends ["response-only"] ? never : never>;
3479
+ }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("@sealant/api-contracts").BudgetExceededError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadGatewayError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceDockerServiceUnsupportedError | import("@sealant/api-contracts").WorkspaceForbiddenError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError | import("@sealant/api-contracts").WorkspaceRuntimeEnvReferencesUnsupportedError | import("@sealant/api-contracts").WorkspaceServiceUnavailableError | import("@sealant/api-contracts").WorkspaceSshOwnerRefusedError), [Mode] extends ["response-only"] ? never : never>;
3410
3480
  readonly execWorkspace: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
3411
3481
  readonly params: {
3412
3482
  readonly workspaceId: string;
@@ -3749,6 +3819,7 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
3749
3819
  readonly headers: {
3750
3820
  readonly "x-sealant-gateway-token"?: string | undefined;
3751
3821
  readonly "x-sealant-principal-id"?: string | undefined;
3822
+ readonly "x-sealant-gateway-ssh-user"?: string | undefined;
3752
3823
  };
3753
3824
  readonly responseMode?: Mode;
3754
3825
  }) => Effect.Effect<HttpApiClient.Client.Response<{
@@ -3761,6 +3832,7 @@ declare const SealantApiClient_base: Context.ServiceClass<SealantApiClient, "@se
3761
3832
  readonly status: "failed" | "pending" | "ready" | "running" | "stopped";
3762
3833
  readonly endpoint: string;
3763
3834
  };
3835
+ readonly sessionUser: string | null;
3764
3836
  }, Mode>, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | ([Mode] extends ["response-only"] ? never : import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError | import("@sealant/api-contracts").WorkspaceServiceUnavailableError | import("@sealant/api-contracts").WorkspaceUnauthorizedError), [Mode] extends ["response-only"] ? never : never>;
3765
3837
  readonly inspectWorkspaceImage: <Mode extends import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode = import("effect/unstable/httpapi/HttpApiEndpoint").ClientResponseMode>(request: {
3766
3838
  readonly payload: {
@@ -32,6 +32,7 @@ export declare const createWorkspaceOp: (payload: {
32
32
  readonly ttlSeconds?: number | undefined;
33
33
  readonly idempotencyKey?: string | undefined;
34
34
  readonly launchId?: string | undefined;
35
+ readonly sshAsOwner?: boolean | undefined;
35
36
  }, idempotencyKey?: string | undefined) => Effect.Effect<{
36
37
  readonly workspaceId: string;
37
38
  readonly name: string;
@@ -52,7 +53,7 @@ export declare const createWorkspaceOp: (payload: {
52
53
  } | undefined;
53
54
  readonly replayed?: boolean | undefined;
54
55
  readonly launchId?: string | undefined;
55
- }, import("@sealant/api-contracts").BudgetExceededError | import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadGatewayError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceDockerServiceUnsupportedError | import("@sealant/api-contracts").WorkspaceForbiddenError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError | import("@sealant/api-contracts").WorkspaceRuntimeEnvReferencesUnsupportedError | import("@sealant/api-contracts").WorkspaceServiceUnavailableError, SealantApiClient>;
56
+ }, import("@sealant/api-contracts").BudgetExceededError | import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadGatewayError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceConflictError | import("@sealant/api-contracts").WorkspaceDockerServiceUnsupportedError | import("@sealant/api-contracts").WorkspaceForbiddenError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError | import("@sealant/api-contracts").WorkspaceRuntimeEnvReferencesUnsupportedError | import("@sealant/api-contracts").WorkspaceServiceUnavailableError | import("@sealant/api-contracts").WorkspaceSshOwnerRefusedError, SealantApiClient>;
56
57
  export declare const getWorkspaceOp: (workspaceId: string, ownerUserId?: string | undefined) => Effect.Effect<{
57
58
  readonly workspaceId: string;
58
59
  readonly name: string;
@@ -644,6 +645,12 @@ export declare const expireWorkspaceOp: (workspaceId: string, payload: {
644
645
  readonly workspaceId: string;
645
646
  readonly expiresAt: string | null;
646
647
  }, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceBadRequestError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError, SealantApiClient>;
648
+ export declare const clearWorkspaceSshUserOp: (workspaceId: string, query: {
649
+ readonly ownerUserId?: string | undefined;
650
+ }) => Effect.Effect<{
651
+ readonly workspaceId: string;
652
+ readonly sshUser: null;
653
+ }, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").WorkspaceInternalServerError | import("@sealant/api-contracts").WorkspaceNotFoundError, SealantApiClient>;
647
654
  export declare const createRunOp: (payload: {
648
655
  readonly workspaceId: string;
649
656
  readonly harnessId: string;
@@ -1062,6 +1069,19 @@ export declare const ensureUserOp: (payload: {
1062
1069
  readonly createdAt: string;
1063
1070
  readonly created: boolean;
1064
1071
  }, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").UserBadRequestError | import("@sealant/api-contracts").UserInternalServerError, SealantApiClient>;
1072
+ export declare const bindUserPersonOp: (userId: string, payload: {
1073
+ readonly id: string;
1074
+ readonly uid: number;
1075
+ readonly home: string;
1076
+ }) => Effect.Effect<{
1077
+ readonly userId: string;
1078
+ readonly person: {
1079
+ readonly id: string;
1080
+ readonly uid: number;
1081
+ readonly home: string;
1082
+ };
1083
+ readonly created: boolean;
1084
+ }, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | import("effect/Schema").SchemaError | import("@sealant/api-contracts").UserBadRequestError | import("@sealant/api-contracts").UserInternalServerError | import("@sealant/api-contracts").UserNotFoundError | import("@sealant/api-contracts").UserPersonConflictError, SealantApiClient>;
1065
1085
  export declare const getUserOp: (userId: string) => Effect.Effect<{
1066
1086
  readonly userId: string;
1067
1087
  readonly email: string;
@@ -1195,6 +1215,8 @@ export declare const getIndexOp: () => Effect.Effect<{
1195
1215
  readonly credentialsPartialPut?: boolean | undefined;
1196
1216
  readonly credentialsPiOpencode?: boolean | undefined;
1197
1217
  readonly captureOwnerMap?: boolean | undefined;
1218
+ readonly workspaceSshUser?: boolean | undefined;
1219
+ readonly personBinding?: boolean | undefined;
1198
1220
  } | undefined;
1199
1221
  }, import("effect/unstable/http/HttpClientError").HttpClientError | import("@sealant/api-contracts").RequestRefusedError | import("effect/Schema").SchemaError, SealantApiClient>;
1200
1222
  export declare const getSetupStateOp: () => Effect.Effect<{
@@ -31,6 +31,7 @@ export const putWorkspaceCredentialsOp = (workspaceId, payload) => Effect.flatMa
31
31
  export const releaseWorkspaceCredentialsOp = (workspaceId, query) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.releaseWorkspaceCredentials({ params: { workspaceId }, query }));
32
32
  export const listWorkspaceCredentialsOp = (workspaceId, query) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.listWorkspaceCredentials({ params: { workspaceId }, query }));
33
33
  export const expireWorkspaceOp = (workspaceId, payload) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.expireWorkspace({ params: { workspaceId }, payload }));
34
+ export const clearWorkspaceSshUserOp = (workspaceId, query) => Effect.flatMap(SealantApiClient, (client) => client.workspaces.clearWorkspaceSshUser({ params: { workspaceId }, query }));
34
35
  // ---- runs ----
35
36
  export const createRunOp = (payload) => Effect.flatMap(SealantApiClient, (client) => client.runs.createRun({ payload }));
36
37
  const ownerQuery = (ownerUserId) => ownerUserId === undefined ? {} : { ownerUserId };
@@ -60,6 +61,7 @@ export const closeSessionOp = (sessionId, payload) => Effect.flatMap(SealantApiC
60
61
  export const createAccessTokenOp = (payload) => Effect.flatMap(SealantApiClient, (client) => client.accessTokens.createAccessToken({ payload }));
61
62
  // ---- users ----
62
63
  export const ensureUserOp = (payload) => Effect.flatMap(SealantApiClient, (client) => client.users.ensureUser({ payload }));
64
+ export const bindUserPersonOp = (userId, payload) => Effect.flatMap(SealantApiClient, (client) => client.users.bindUserPerson({ params: { userId }, payload }));
63
65
  export const getUserOp = (userId) => Effect.flatMap(SealantApiClient, (client) => client.users.getUser({ params: { userId } }));
64
66
  // ---- connected accounts ----
65
67
  export const listConnectedAccountsOp = (ownerUserId) => Effect.flatMap(SealantApiClient, (client) => client.connectedAccounts.listConnectedAccounts({ query: { ownerUserId } }));
@@ -1,7 +1,7 @@
1
1
  import { sessionArgvIssue } from "@sealant/api-contracts";
2
2
  import { applyDotfiles } from "../effect/apply-dotfiles.js";
3
3
  import { execWorkspace } from "../effect/exec-workspace.js";
4
- import { bindWorkspaceOp, flushWorkspaceCaptureOp, getWorkspaceCaptureStatusOp, createSessionAsUserOp, createSessionOp, expireWorkspaceOp, getSessionOp, getWorkspaceOp, listSessionsOp, replanWorkspaceCaptureOp, recoverWorkspaceOp, restartWorkspaceOp, listWorkspaceCredentialsOp, putWorkspaceCredentialsOp, releaseWorkspaceCredentialsOp, stopWorkspaceOp, } from "../effect/operations.js";
4
+ import { bindWorkspaceOp, flushWorkspaceCaptureOp, getWorkspaceCaptureStatusOp, createSessionAsUserOp, createSessionOp, expireWorkspaceOp, clearWorkspaceSshUserOp, getSessionOp, getWorkspaceOp, listSessionsOp, replanWorkspaceCaptureOp, recoverWorkspaceOp, restartWorkspaceOp, listWorkspaceCredentialsOp, putWorkspaceCredentialsOp, releaseWorkspaceCredentialsOp, stopWorkspaceOp, } from "../effect/operations.js";
5
5
  import { SealantApiError, SealantError, SealantNotImplementedError } from "../errors.js";
6
6
  import { mapAccountRef } from "../internal/credentials.js";
7
7
  import { parseTtlSeconds } from "../internal/duration.js";
@@ -789,6 +789,9 @@ export const makeWorkspace = (ctx, init) => {
789
789
  ...(ttl === undefined ? {} : { ttlSeconds: ttl === null ? null : parseTtlSeconds(ttl) }),
790
790
  }));
791
791
  },
792
+ sshAsRoot: async () => {
793
+ await ctx.runtime.run(clearWorkspaceSshUserOp(init.id, { ownerUserId: ctx.config.hostLocal.ownerUserId }));
794
+ },
792
795
  forward: (port, options) => openForward(ctx, init.id, port, options),
793
796
  };
794
797
  return workspace;
@@ -34,5 +34,6 @@ export declare const buildCreateWorkspaceRequest: (options: CreateOptions, confi
34
34
  readonly ttlSeconds?: number | undefined;
35
35
  readonly idempotencyKey?: string | undefined;
36
36
  readonly launchId?: string | undefined;
37
+ readonly sshAsOwner?: boolean | undefined;
37
38
  };
38
39
  };
@@ -267,6 +267,7 @@ export const buildCreateWorkspaceRequest = (options, config) => {
267
267
  ...(options.ttl === undefined ? {} : { ttlSeconds: parseTtlSeconds(options.ttl) }),
268
268
  ...(options.idempotencyKey === undefined ? {} : { idempotencyKey: options.idempotencyKey }),
269
269
  ...(options.launchId === undefined ? {} : { launchId: options.launchId }),
270
+ ...(options.sshAsOwner === true ? { sshAsOwner: true } : {}),
270
271
  spec,
271
272
  ...(secretEnv === undefined ? {} : { secretEnv }),
272
273
  // The capture credential is the one secret outside `secretEnv`: sealed and delivered the
@@ -13,6 +13,8 @@ export declare const toFeatures: (index: {
13
13
  readonly credentialsPartialPut?: boolean | undefined;
14
14
  readonly credentialsPiOpencode?: boolean | undefined;
15
15
  readonly captureOwnerMap?: boolean | undefined;
16
+ readonly workspaceSshUser?: boolean | undefined;
17
+ readonly personBinding?: boolean | undefined;
16
18
  } | undefined;
17
19
  }) => SealantFeatures;
18
20
  /**
@@ -15,6 +15,8 @@ export const toFeatures = (index) => {
15
15
  credentialsPartialPut: features?.credentialsPartialPut === true,
16
16
  credentialsPiOpencode: features?.credentialsPiOpencode === true,
17
17
  captureOwnerMap: features?.captureOwnerMap === true,
18
+ workspaceSshUser: features?.workspaceSshUser === true,
19
+ personBinding: features?.personBinding === true,
18
20
  };
19
21
  };
20
22
  /**
@@ -1,2 +1,8 @@
1
1
  import type { SdkContext } from "../facade/context.js";
2
+ /**
3
+ * A workspace's SSH user is asked only of a control plane that reports it takes one
4
+ * (`features.workspaceSshUser`): an older one ignores `sshAsOwner` on a create and its gateway
5
+ * would run the sessions as root.
6
+ */
7
+ export declare const requireSshUser: (ctx: SdkContext) => Promise<void>;
2
8
  export declare const requireProcessUser: (ctx: SdkContext, user: string) => Promise<void>;
@@ -8,6 +8,17 @@
8
8
  */
9
9
  import { SealantError } from "../errors.js";
10
10
  import { readFeatures } from "./features.js";
11
+ /**
12
+ * A workspace's SSH user is asked only of a control plane that reports it takes one
13
+ * (`features.workspaceSshUser`): an older one ignores `sshAsOwner` on a create and its gateway
14
+ * would run the sessions as root.
15
+ */
16
+ export const requireSshUser = async (ctx) => {
17
+ const supported = await readFeatures(ctx).then((features) => features.workspaceSshUser, () => false);
18
+ if (!supported) {
19
+ throw new SealantError("This control plane cannot run SSH sessions as the owner's Linux user (it does not report the feature, or could not be asked); nothing was sent.", { code: "ssh-user-unsupported" });
20
+ }
21
+ };
11
22
  export const requireProcessUser = async (ctx, user) => {
12
23
  const supported = await readFeatures(ctx).then((features) => features.processUserRoutes, () => false);
13
24
  if (!supported) {
package/dist/types.d.ts CHANGED
@@ -883,6 +883,15 @@ export interface CreateOptions {
883
883
  * the logins are written by the launch itself.
884
884
  */
885
885
  readonly credentialsHome?: WorkspaceCredentialsHomeOptions;
886
+ /**
887
+ * The SSH gateway runs this workspace's SSH sessions (VS Code Remote-SSH, an `ssh` shell or
888
+ * command) as its owner's own Linux user: the uid of `credentialsHome`, which this requires
889
+ * (40001–49999, never root). No caller names the user. It need not exist yet; until it does, a
890
+ * session is refused, never run as root. `workspace.sshAsRoot()` sets it back to root. Absent:
891
+ * root. Sent only to a control plane that reports `features().workspaceSshUser`; with another,
892
+ * `create` rejects (`ssh-user-unsupported`) and nothing is created.
893
+ */
894
+ readonly sshAsOwner?: boolean;
886
895
  /**
887
896
  * Time-to-live for the workspace, e.g. `"90m"`, `"2h"` (also `"45s"`, `"1d"`). Once it elapses
888
897
  * the platform stops the workspace and removes its container. Omitted = the server default TTL
@@ -1118,6 +1127,12 @@ export interface Workspace {
1118
1127
  expire(options?: {
1119
1128
  readonly in?: string | null;
1120
1129
  }): Promise<void>;
1130
+ /**
1131
+ * The SSH gateway runs this workspace's SSH sessions as root from the next session channel on,
1132
+ * open connections included (`DELETE /v1/workspaces/:id/ssh-user`). The only change after
1133
+ * create: a workspace's user is its owner's, set by `sshAsOwner`, never named by a caller.
1134
+ */
1135
+ sshAsRoot(): Promise<void>;
1121
1136
  /**
1122
1137
  * Open a raw TCP byte pipe (or a UDP datagram pipe) INSIDE the workspace — the primitive for
1123
1138
  * reaching a dev server or database the workspace runs. Protocol-agnostic:
@@ -1353,6 +1368,10 @@ export interface SealantFeatures {
1353
1368
  readonly credentialsPiOpencode: boolean;
1354
1369
  /** `ownerMap` on a capture source. */
1355
1370
  readonly captureOwnerMap: boolean;
1371
+ /** `sshAsOwner` on a create and `workspace.sshAsRoot()`: SSH sessions as the owner's user. */
1372
+ readonly workspaceSshUser: boolean;
1373
+ /** `users.bindPerson()`: a user's person, bound once. */
1374
+ readonly personBinding: boolean;
1356
1375
  }
1357
1376
  /** A published workspace image. */
1358
1377
  export interface WorkspaceImage {
@@ -2010,9 +2029,29 @@ export interface EnsuredUser extends SealantUser {
2010
2029
  * Identity rows for products that own their own login. `ensure` is idempotent on email: call it on
2011
2030
  * every sign-in and build the per-user client with the returned `userId` as `ownerUserId`.
2012
2031
  */
2032
+ /**
2033
+ * The person a user is (Mend ADR 0016): their id in a capture owner map, their uid (40001–49999)
2034
+ * and their home, under the deployment's homes root.
2035
+ */
2036
+ export interface UserPerson {
2037
+ readonly id: string;
2038
+ readonly uid: number;
2039
+ readonly home: string;
2040
+ }
2013
2041
  export interface UsersNamespace {
2014
2042
  ensure(options: EnsureUserOptions): Promise<EnsuredUser>;
2015
2043
  get(userId: string): Promise<SealantUser>;
2044
+ /**
2045
+ * Binds the user to a person, once (what `create({ sshAsOwner })` checks a create against). The
2046
+ * same values again answer `created: false`; another person for the user, or a person id or uid
2047
+ * another user holds, rejects (409, `reason` `person-binding-differs` or `person-taken`) and
2048
+ * changes nothing. Needs a service key and `features().personBinding`.
2049
+ */
2050
+ bindPerson(userId: string, person: UserPerson): Promise<{
2051
+ readonly userId: string;
2052
+ readonly person: UserPerson;
2053
+ readonly created: boolean;
2054
+ }>;
2016
2055
  }
2017
2056
  export type ConnectedAccountProvider = "claude" | "codex" | "github";
2018
2057
  export type ConnectedAccountStatus = "active" | "invalid" | "archived";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealant/sdk-next",
3
- "version": "0.39.0-next.712",
3
+ "version": "0.39.0-next.714",
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,12 +26,12 @@
26
26
  "node": ">=20"
27
27
  },
28
28
  "dependencies": {
29
- "@sealant/api-contracts": "npm:@sealant/api-contracts-next@0.39.0-next.712"
29
+ "@sealant/api-contracts": "npm:@sealant/api-contracts-next@0.39.0-next.714"
30
30
  },
31
31
  "peerDependencies": {
32
32
  "effect": ">=4.0.0-beta.85 <5"
33
33
  },
34
- "gitHead": "43bed8e5b68a8412670ea54770a7d203c6b0a554",
34
+ "gitHead": "6da21a94e3f8650df5cbdb53626f420981cd1831",
35
35
  "publishConfig": {
36
36
  "access": "public"
37
37
  }