@sealant/api-contracts-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.
@@ -33,6 +33,13 @@ export declare const systemIndexResponseSchema: Schema.Struct<{
33
33
  readonly credentialsPiOpencode: Schema.optional<Schema.Boolean>;
34
34
  /** `ownerMap` on a capture source: an executor whose restore gives each person their files. */
35
35
  readonly captureOwnerMap: Schema.optional<Schema.Boolean>;
36
+ /**
37
+ * `sshAsOwner` on a create and `DELETE /v1/workspaces/:id/ssh-user`: the SSH gateway runs a
38
+ * workspace's SSH sessions as its owner's own Linux user (their `credentialsHome` uid).
39
+ */
40
+ readonly workspaceSshUser: Schema.optional<Schema.Boolean>;
41
+ /** `POST /v1/users/:id/person`: a user's person bound once (what `sshAsOwner` checks). */
42
+ readonly personBinding: Schema.optional<Schema.Boolean>;
36
43
  }>>;
37
44
  }>;
38
45
  export type SystemIndexResponse = typeof systemIndexResponseSchema.Type;
@@ -93,6 +100,13 @@ export declare const SystemGroup: HttpApiGroup.HttpApiGroup<"system", HttpApiEnd
93
100
  readonly credentialsPiOpencode: Schema.optional<Schema.Boolean>;
94
101
  /** `ownerMap` on a capture source: an executor whose restore gives each person their files. */
95
102
  readonly captureOwnerMap: Schema.optional<Schema.Boolean>;
103
+ /**
104
+ * `sshAsOwner` on a create and `DELETE /v1/workspaces/:id/ssh-user`: the SSH gateway runs a
105
+ * workspace's SSH sessions as its owner's own Linux user (their `credentialsHome` uid).
106
+ */
107
+ readonly workspaceSshUser: Schema.optional<Schema.Boolean>;
108
+ /** `POST /v1/users/:id/person`: a user's person bound once (what `sshAsOwner` checks). */
109
+ readonly personBinding: Schema.optional<Schema.Boolean>;
96
110
  }>>;
97
111
  }>>, HttpApiEndpoint.Json<never>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getSetupState", "GET", "/v1/system/setup-state", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
98
112
  readonly needsSetup: Schema.Boolean;
@@ -34,6 +34,13 @@ export const systemIndexResponseSchema = Schema.Struct({
34
34
  credentialsPiOpencode: Schema.optional(Schema.Boolean),
35
35
  /** `ownerMap` on a capture source: an executor whose restore gives each person their files. */
36
36
  captureOwnerMap: Schema.optional(Schema.Boolean),
37
+ /**
38
+ * `sshAsOwner` on a create and `DELETE /v1/workspaces/:id/ssh-user`: the SSH gateway runs a
39
+ * workspace's SSH sessions as its owner's own Linux user (their `credentialsHome` uid).
40
+ */
41
+ workspaceSshUser: Schema.optional(Schema.Boolean),
42
+ /** `POST /v1/users/:id/person`: a user's person bound once (what `sshAsOwner` checks). */
43
+ personBinding: Schema.optional(Schema.Boolean),
37
44
  })),
38
45
  });
39
46
  export const systemHealthResponseSchema = Schema.Struct({
@@ -32,6 +32,39 @@ export declare const ensureUserResponseSchema: Schema.Struct<{
32
32
  readonly created: Schema.Boolean;
33
33
  }>;
34
34
  export type EnsureUserResponse = typeof ensureUserResponseSchema.Type;
35
+ /**
36
+ * The person a user is (Mend ADR 0016: a person's Linux identity, the same in every workspace):
37
+ * their id in a capture owner map, their uid (40001–49999) and their home, an absolute normalised
38
+ * path under the deployment's homes root (`SEALANT_PERSON_HOMES_ROOT`, default `/home`).
39
+ */
40
+ export declare const personBindingSchema: Schema.Struct<{
41
+ readonly id: Schema.String;
42
+ readonly uid: Schema.Int;
43
+ readonly home: Schema.String;
44
+ }>;
45
+ export type PersonBindingWire = typeof personBindingSchema.Type;
46
+ export declare const bindUserPersonResponseSchema: Schema.Struct<{
47
+ readonly userId: Schema.String;
48
+ readonly person: Schema.Struct<{
49
+ readonly id: Schema.String;
50
+ readonly uid: Schema.Int;
51
+ readonly home: Schema.String;
52
+ }>;
53
+ /** True when this call made the binding; false when it was already exactly this. */
54
+ readonly created: Schema.Boolean;
55
+ }>;
56
+ export type BindUserPersonResponse = typeof bindUserPersonResponseSchema.Type;
57
+ declare const UserPersonConflictError_base: Schema.Class<UserPersonConflictError, Schema.TaggedStruct<"UserPersonConflictError", {
58
+ readonly message: Schema.String;
59
+ readonly code: Schema.Literals<readonly ["person-binding-differs", "person-taken"]>;
60
+ }>, import("effect/Cause").YieldableError>;
61
+ /**
62
+ * A binding that would change one already made: the user is bound to another person
63
+ * (`person-binding-differs`), or the person id or uid is another user's (`person-taken`). A
64
+ * binding is never changed through the API; the answer names no other user's values.
65
+ */
66
+ export declare class UserPersonConflictError extends UserPersonConflictError_base {
67
+ }
35
68
  declare const UserBadRequestError_base: Schema.Class<UserBadRequestError, Schema.TaggedStruct<"UserBadRequestError", {
36
69
  readonly message: Schema.String;
37
70
  }>, import("effect/Cause").YieldableError>;
@@ -47,7 +80,22 @@ declare const UserInternalServerError_base: Schema.Class<UserInternalServerError
47
80
  }>, import("effect/Cause").YieldableError>;
48
81
  export declare class UserInternalServerError extends UserInternalServerError_base {
49
82
  }
50
- export declare const UsersGroup: HttpApiGroup.HttpApiGroup<"users", HttpApiEndpoint.HttpApiEndpoint<"ensureUser", "POST", "/", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
83
+ export declare const UsersGroup: HttpApiGroup.HttpApiGroup<"users", HttpApiEndpoint.HttpApiEndpoint<"bindUserPerson", "POST", "/:userId/person", HttpApiEndpoint.StringTree<Schema.Struct<{
84
+ readonly userId: Schema.String;
85
+ }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
86
+ readonly id: Schema.String;
87
+ readonly uid: Schema.Int;
88
+ readonly home: Schema.String;
89
+ }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
90
+ readonly userId: Schema.String;
91
+ readonly person: Schema.Struct<{
92
+ readonly id: Schema.String;
93
+ readonly uid: Schema.Int;
94
+ readonly home: Schema.String;
95
+ }>;
96
+ /** True when this call made the binding; false when it was already exactly this. */
97
+ readonly created: Schema.Boolean;
98
+ }>>, HttpApiEndpoint.Json<typeof UserPersonConflictError | typeof UserBadRequestError | typeof UserNotFoundError | typeof UserInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"ensureUser", "POST", "/", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
51
99
  /** Matched case-insensitively; the stored form is lower-cased. */
52
100
  readonly email: Schema.String;
53
101
  readonly name: Schema.String;
@@ -27,6 +27,32 @@ export const ensureUserResponseSchema = Schema.Struct({
27
27
  /** True when this call created the user. */
28
28
  created: Schema.Boolean,
29
29
  });
30
+ /**
31
+ * The person a user is (Mend ADR 0016: a person's Linux identity, the same in every workspace):
32
+ * their id in a capture owner map, their uid (40001–49999) and their home, an absolute normalised
33
+ * path under the deployment's homes root (`SEALANT_PERSON_HOMES_ROOT`, default `/home`).
34
+ */
35
+ export const personBindingSchema = Schema.Struct({
36
+ id: NonEmptyString,
37
+ uid: Schema.Int,
38
+ home: NonEmptyString,
39
+ });
40
+ export const bindUserPersonResponseSchema = Schema.Struct({
41
+ userId: NonEmptyString,
42
+ person: personBindingSchema,
43
+ /** True when this call made the binding; false when it was already exactly this. */
44
+ created: Schema.Boolean,
45
+ });
46
+ /**
47
+ * A binding that would change one already made: the user is bound to another person
48
+ * (`person-binding-differs`), or the person id or uid is another user's (`person-taken`). A
49
+ * binding is never changed through the API; the answer names no other user's values.
50
+ */
51
+ export class UserPersonConflictError extends Schema.TaggedErrorClass()("UserPersonConflictError", {
52
+ message: Schema.String,
53
+ code: Schema.Literals(["person-binding-differs", "person-taken"]),
54
+ }, { httpApiStatus: 409 }) {
55
+ }
30
56
  export class UserBadRequestError extends Schema.TaggedErrorClass()("UserBadRequestError", { message: Schema.String }, { httpApiStatus: 400 }) {
31
57
  }
32
58
  export class UserNotFoundError extends Schema.TaggedErrorClass()("UserNotFoundError", { message: Schema.String }, { httpApiStatus: 404 }) {
@@ -39,6 +65,17 @@ export const UsersGroup = HttpApiGroup.make("users")
39
65
  payload: ensureUserRequestSchema,
40
66
  success: ensureUserResponseSchema,
41
67
  error: [UserBadRequestError, UserInternalServerError],
68
+ }))
69
+ .add(HttpApiEndpoint.post("bindUserPerson", "/:userId/person", {
70
+ params: userIdParams,
71
+ payload: personBindingSchema,
72
+ success: bindUserPersonResponseSchema,
73
+ error: [
74
+ UserBadRequestError,
75
+ UserNotFoundError,
76
+ UserPersonConflictError,
77
+ UserInternalServerError,
78
+ ],
42
79
  }))
43
80
  .add(HttpApiEndpoint.get("getUser", "/:userId", {
44
81
  params: userIdParams,
@@ -45,6 +45,14 @@ export declare const workspaceSshTargetSchema: Schema.Struct<{
45
45
  readonly status: Schema.Literals<readonly ["pending", "running", "ready", "failed", "stopped"]>;
46
46
  readonly endpoint: Schema.String;
47
47
  }>;
48
+ /**
49
+ * Who the gateway runs this workspace's SSH sessions as, always stated: the workspace owner's
50
+ * Linux user (a decimal uid, from the owner's `credentialsHome`), or `null` for root. A gateway
51
+ * refuses a target that does not state it (an older API, which knows no SSH user), never reads
52
+ * the silence as root. Sent only to a gateway that says it runs sessions as a user
53
+ * (`x-sealant-gateway-ssh-user: 1`); a workspace with a user refuses any other gateway.
54
+ */
55
+ readonly sessionUser: Schema.NullOr<Schema.String>;
48
56
  }>;
49
57
  export type WorkspaceSshTarget = typeof workspaceSshTargetSchema.Type;
50
58
  /**
@@ -160,6 +168,15 @@ export declare const createWorkspaceRequestSchema: Schema.Struct<{
160
168
  * completion attestation that names a launch must name this one.
161
169
  */
162
170
  readonly launchId: Schema.optional<Schema.String>;
171
+ /**
172
+ * The SSH gateway runs this workspace's SSH sessions as its owner's own Linux user: the person
173
+ * the owner is bound to (`POST /v1/users/:id/person`). The create must carry a capture owner map
174
+ * giving that person their bound uid, and `runtime.credentialsHome` must be their bound uid and
175
+ * home; otherwise 403 (`WorkspaceSshOwnerRefusedError`). The caller names no user. The user need not exist
176
+ * yet: until it does, an SSH session is refused, never run as root. Absent or false: root. A
177
+ * control plane reports it takes this with `features.workspaceSshUser`; an older one ignores it.
178
+ */
179
+ readonly sshAsOwner: Schema.optional<Schema.Boolean>;
163
180
  }>;
164
181
  export type CreateWorkspaceRequest = typeof createWorkspaceRequestSchema.Type;
165
182
  export declare const createWorkspaceHeadersSchema: Schema.Struct<{
@@ -579,6 +596,15 @@ export declare const renameWorkspaceRequestSchema: Schema.Struct<{
579
596
  readonly ownerUserId: Schema.optional<Schema.String>;
580
597
  }>;
581
598
  export type RenameWorkspaceRequest = typeof renameWorkspaceRequestSchema.Type;
599
+ /**
600
+ * `DELETE /v1/workspaces/:id/ssh-user`: the gateway runs the workspace's SSH sessions as root from
601
+ * the next session channel on. The only change after create: no caller ever names a user.
602
+ */
603
+ export declare const clearWorkspaceSshUserQuerySchema: Schema.Struct<{
604
+ /** The workspace's owner. Required by the control plane: a request that names none finds nothing. */
605
+ readonly ownerUserId: Schema.optional<Schema.String>;
606
+ }>;
607
+ export type ClearWorkspaceSshUserQuery = typeof clearWorkspaceSshUserQuerySchema.Type;
582
608
  export declare const stopWorkspaceRequestSchema: Schema.Struct<{
583
609
  readonly ownerUserId: Schema.String;
584
610
  /**
@@ -1007,6 +1033,12 @@ export declare const renameWorkspaceResponseSchema: Schema.Struct<{
1007
1033
  readonly updatedAt: Schema.String;
1008
1034
  }>;
1009
1035
  export type RenameWorkspaceResponse = typeof renameWorkspaceResponseSchema.Type;
1036
+ export declare const clearWorkspaceSshUserResponseSchema: Schema.Struct<{
1037
+ readonly workspaceId: Schema.String;
1038
+ /** Always `null` (root) after the call. */
1039
+ readonly sshUser: Schema.Null;
1040
+ }>;
1041
+ export type ClearWorkspaceSshUserResponse = typeof clearWorkspaceSshUserResponseSchema.Type;
1010
1042
  export declare const createWorkspaceResponseSchema: Schema.Struct<{
1011
1043
  readonly workspaceId: Schema.String;
1012
1044
  readonly name: Schema.String;
@@ -1676,6 +1708,7 @@ export type ListWorkspaceEventsResponse = typeof listWorkspaceEventsResponseSche
1676
1708
  export declare const workspaceGatewayHeadersSchema: Schema.Struct<{
1677
1709
  readonly "x-sealant-gateway-token": Schema.optional<Schema.String>;
1678
1710
  readonly "x-sealant-principal-id": Schema.optional<Schema.String>;
1711
+ readonly "x-sealant-gateway-ssh-user": Schema.optional<Schema.String>;
1679
1712
  }>;
1680
1713
  export type WorkspaceGatewayHeaders = typeof workspaceGatewayHeadersSchema.Type;
1681
1714
  declare const WorkspaceBadRequestError_base: Schema.Class<WorkspaceBadRequestError, Schema.TaggedStruct<"WorkspaceBadRequestError", {
@@ -1714,6 +1747,17 @@ declare const WorkspaceUnauthorizedError_base: Schema.Class<WorkspaceUnauthorize
1714
1747
  }>, import("effect/Cause").YieldableError>;
1715
1748
  export declare class WorkspaceUnauthorizedError extends WorkspaceUnauthorizedError_base {
1716
1749
  }
1750
+ declare const WorkspaceSshOwnerRefusedError_base: Schema.Class<WorkspaceSshOwnerRefusedError, Schema.TaggedStruct<"WorkspaceSshOwnerRefusedError", {
1751
+ readonly message: Schema.String;
1752
+ readonly code: Schema.Literals<readonly ["ssh-owner-unbound", "ssh-owner-needs-owner-map", "ssh-owner-mismatch"]>;
1753
+ }>, import("effect/Cause").YieldableError>;
1754
+ /**
1755
+ * `sshAsOwner` refused: the owner is bound to no person (`ssh-owner-unbound`), the create carries no
1756
+ * capture owner map (`ssh-owner-needs-owner-map`), or the owner map or `credentialsHome` does not
1757
+ * give the owner's bound person their own uid and home (`ssh-owner-mismatch`). Names no path.
1758
+ */
1759
+ export declare class WorkspaceSshOwnerRefusedError extends WorkspaceSshOwnerRefusedError_base {
1760
+ }
1717
1761
  declare const WorkspaceForbiddenError_base: Schema.Class<WorkspaceForbiddenError, Schema.TaggedStruct<"WorkspaceForbiddenError", {
1718
1762
  readonly message: Schema.String;
1719
1763
  }>, import("effect/Cause").YieldableError>;
@@ -1830,7 +1874,16 @@ export declare const WorkspacesGroup: HttpApiGroup.HttpApiGroup<"workspaces", Ht
1830
1874
  readonly workspaceId: Schema.optional<Schema.String>;
1831
1875
  readonly runId: Schema.optional<Schema.String>;
1832
1876
  readonly launchId: Schema.optional<Schema.String>;
1833
- }>>, HttpApiEndpoint.Json<typeof WorkspaceBadRequestError | typeof WorkspaceInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"createWorkspace", "POST", "/", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
1877
+ }>>, HttpApiEndpoint.Json<typeof WorkspaceBadRequestError | typeof WorkspaceInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"clearWorkspaceSshUser", "DELETE", "/:workspaceId/ssh-user", HttpApiEndpoint.StringTree<Schema.Struct<{
1878
+ readonly workspaceId: Schema.String;
1879
+ }>>, HttpApiEndpoint.StringTree<Schema.Struct<{
1880
+ /** The workspace's owner. Required by the control plane: a request that names none finds nothing. */
1881
+ readonly ownerUserId: Schema.optional<Schema.String>;
1882
+ }>>, HttpApiEndpoint.Json<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
1883
+ readonly workspaceId: Schema.String;
1884
+ /** Always `null` (root) after the call. */
1885
+ readonly sshUser: Schema.Null;
1886
+ }>>, HttpApiEndpoint.Json<typeof WorkspaceNotFoundError | typeof WorkspaceInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"createWorkspace", "POST", "/", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
1834
1887
  readonly ownerUserId: Schema.String;
1835
1888
  readonly registryId: Schema.String;
1836
1889
  readonly repository: Schema.String;
@@ -1873,6 +1926,15 @@ export declare const WorkspacesGroup: HttpApiGroup.HttpApiGroup<"workspaces", Ht
1873
1926
  * completion attestation that names a launch must name this one.
1874
1927
  */
1875
1928
  readonly launchId: Schema.optional<Schema.String>;
1929
+ /**
1930
+ * The SSH gateway runs this workspace's SSH sessions as its owner's own Linux user: the person
1931
+ * the owner is bound to (`POST /v1/users/:id/person`). The create must carry a capture owner map
1932
+ * giving that person their bound uid, and `runtime.credentialsHome` must be their bound uid and
1933
+ * home; otherwise 403 (`WorkspaceSshOwnerRefusedError`). The caller names no user. The user need not exist
1934
+ * yet: until it does, an SSH session is refused, never run as root. Absent or false: root. A
1935
+ * control plane reports it takes this with `features.workspaceSshUser`; an older one ignores it.
1936
+ */
1937
+ readonly sshAsOwner: Schema.optional<Schema.Boolean>;
1876
1938
  }>>, HttpApiEndpoint.StringTree<Schema.Struct<{
1877
1939
  readonly "idempotency-key": Schema.optional<Schema.String>;
1878
1940
  }>>, HttpApiEndpoint.Json<Schema.Struct<{
@@ -1915,7 +1977,7 @@ export declare const WorkspacesGroup: HttpApiGroup.HttpApiGroup<"workspaces", Ht
1915
1977
  readonly replayed: Schema.optional<Schema.Boolean>;
1916
1978
  /** The launch identity the create named (`launchId`), recorded on its attempt. */
1917
1979
  readonly launchId: Schema.optional<Schema.String>;
1918
- }>>, HttpApiEndpoint.Json<typeof BudgetExceededError | typeof WorkspaceBadRequestError | typeof WorkspaceRuntimeEnvReferencesUnsupportedError | typeof WorkspaceDockerServiceUnsupportedError | typeof WorkspaceForbiddenError | typeof WorkspaceNotFoundError | typeof WorkspaceConflictError | typeof WorkspaceBadGatewayError | typeof WorkspaceServiceUnavailableError | typeof WorkspaceInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"execWorkspace", "POST", "/:workspaceId/exec", HttpApiEndpoint.StringTree<Schema.Struct<{
1980
+ }>>, HttpApiEndpoint.Json<typeof BudgetExceededError | typeof WorkspaceBadRequestError | typeof WorkspaceRuntimeEnvReferencesUnsupportedError | typeof WorkspaceDockerServiceUnsupportedError | typeof WorkspaceSshOwnerRefusedError | typeof WorkspaceForbiddenError | typeof WorkspaceNotFoundError | typeof WorkspaceConflictError | typeof WorkspaceBadGatewayError | typeof WorkspaceServiceUnavailableError | typeof WorkspaceInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"execWorkspace", "POST", "/:workspaceId/exec", HttpApiEndpoint.StringTree<Schema.Struct<{
1919
1981
  readonly workspaceId: Schema.String;
1920
1982
  }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
1921
1983
  readonly ownerUserId: Schema.String;
@@ -2429,6 +2491,7 @@ export declare const WorkspacesGroup: HttpApiGroup.HttpApiGroup<"workspaces", Ht
2429
2491
  }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<Schema.Struct<{
2430
2492
  readonly "x-sealant-gateway-token": Schema.optional<Schema.String>;
2431
2493
  readonly "x-sealant-principal-id": Schema.optional<Schema.String>;
2494
+ readonly "x-sealant-gateway-ssh-user": Schema.optional<Schema.String>;
2432
2495
  }>>, HttpApiEndpoint.Json<Schema.Struct<{
2433
2496
  readonly workspaceId: Schema.String;
2434
2497
  readonly attemptId: Schema.String;
@@ -2439,6 +2502,14 @@ export declare const WorkspacesGroup: HttpApiGroup.HttpApiGroup<"workspaces", Ht
2439
2502
  readonly status: Schema.Literals<readonly ["pending", "running", "ready", "failed", "stopped"]>;
2440
2503
  readonly endpoint: Schema.String;
2441
2504
  }>;
2505
+ /**
2506
+ * Who the gateway runs this workspace's SSH sessions as, always stated: the workspace owner's
2507
+ * Linux user (a decimal uid, from the owner's `credentialsHome`), or `null` for root. A gateway
2508
+ * refuses a target that does not state it (an older API, which knows no SSH user), never reads
2509
+ * the silence as root. Sent only to a gateway that says it runs sessions as a user
2510
+ * (`x-sealant-gateway-ssh-user: 1`); a workspace with a user refuses any other gateway.
2511
+ */
2512
+ readonly sessionUser: Schema.NullOr<Schema.String>;
2442
2513
  }>>, HttpApiEndpoint.Json<typeof WorkspaceUnauthorizedError | typeof WorkspaceNotFoundError | typeof WorkspaceConflictError | typeof WorkspaceServiceUnavailableError | typeof WorkspaceInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"inspectWorkspaceImage", "POST", "/image", HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
2443
2514
  readonly ownerUserId: Schema.String;
2444
2515
  readonly registryId: Schema.String;
@@ -55,6 +55,14 @@ export const workspaceSshTargetSchema = Schema.Struct({
55
55
  status: Schema.Literals(["pending", "running", "ready", "failed", "stopped"]),
56
56
  endpoint: Schema.String,
57
57
  }),
58
+ /**
59
+ * Who the gateway runs this workspace's SSH sessions as, always stated: the workspace owner's
60
+ * Linux user (a decimal uid, from the owner's `credentialsHome`), or `null` for root. A gateway
61
+ * refuses a target that does not state it (an older API, which knows no SSH user), never reads
62
+ * the silence as root. Sent only to a gateway that says it runs sessions as a user
63
+ * (`x-sealant-gateway-ssh-user: 1`); a workspace with a user refuses any other gateway.
64
+ */
65
+ sessionUser: Schema.NullOr(NonEmptyString),
58
66
  });
59
67
  /**
60
68
  * Whether an image can run Mend's per-person layout on a runtime (Mend ADR 0016): the verdict of
@@ -154,6 +162,15 @@ export const createWorkspaceRequestSchema = Schema.Struct({
154
162
  * completion attestation that names a launch must name this one.
155
163
  */
156
164
  launchId: Schema.optional(NonEmptyString),
165
+ /**
166
+ * The SSH gateway runs this workspace's SSH sessions as its owner's own Linux user: the person
167
+ * the owner is bound to (`POST /v1/users/:id/person`). The create must carry a capture owner map
168
+ * giving that person their bound uid, and `runtime.credentialsHome` must be their bound uid and
169
+ * home; otherwise 403 (`WorkspaceSshOwnerRefusedError`). The caller names no user. The user need not exist
170
+ * yet: until it does, an SSH session is refused, never run as root. Absent or false: root. A
171
+ * control plane reports it takes this with `features.workspaceSshUser`; an older one ignores it.
172
+ */
173
+ sshAsOwner: Schema.optional(Schema.Boolean),
157
174
  });
158
175
  export const createWorkspaceHeadersSchema = Schema.Struct({
159
176
  "idempotency-key": Schema.optional(NonEmptyString),
@@ -507,6 +524,14 @@ export const renameWorkspaceRequestSchema = Schema.Struct({
507
524
  /** The workspace's owner. Required by the control plane: a rename that names none finds nothing. */
508
525
  ownerUserId: Schema.optional(NonEmptyString),
509
526
  });
527
+ /**
528
+ * `DELETE /v1/workspaces/:id/ssh-user`: the gateway runs the workspace's SSH sessions as root from
529
+ * the next session channel on. The only change after create: no caller ever names a user.
530
+ */
531
+ export const clearWorkspaceSshUserQuerySchema = Schema.Struct({
532
+ /** The workspace's owner. Required by the control plane: a request that names none finds nothing. */
533
+ ownerUserId: Schema.optional(NonEmptyString),
534
+ });
510
535
  // Lifecycle actions are owner-scoped like execWorkspace: ownerUserId rides in the payload and a
511
536
  // mismatch yields a uniform 404 (existence is not leaked).
512
537
  export const stopWorkspaceRequestSchema = Schema.Struct({
@@ -797,6 +822,11 @@ export const renameWorkspaceResponseSchema = Schema.Struct({
797
822
  name: NonEmptyString,
798
823
  updatedAt: Schema.String,
799
824
  });
825
+ export const clearWorkspaceSshUserResponseSchema = Schema.Struct({
826
+ workspaceId: NonEmptyString,
827
+ /** Always `null` (root) after the call. */
828
+ sshUser: Schema.Null,
829
+ });
800
830
  export const createWorkspaceResponseSchema = Schema.Struct({
801
831
  workspaceId: NonEmptyString,
802
832
  name: NonEmptyString,
@@ -1072,6 +1102,9 @@ export const workspaceGatewayHeadersSchema = Schema.Struct({
1072
1102
  // Identifies the client principal (the SSH key's owner). The API authorizes principal x workspace
1073
1103
  // before returning a control target (gateway-spec §3.4).
1074
1104
  "x-sealant-principal-id": Schema.optional(NonEmptyString),
1105
+ // `1` from a gateway that runs a workspace's SSH sessions as its `sshUser`. A workspace with a
1106
+ // user answers no target to a gateway without it, which would run the session as root.
1107
+ "x-sealant-gateway-ssh-user": Schema.optional(NonEmptyString),
1075
1108
  });
1076
1109
  export class WorkspaceBadRequestError extends Schema.TaggedErrorClass()("WorkspaceBadRequestError", {
1077
1110
  message: Schema.String,
@@ -1105,6 +1138,16 @@ export class WorkspaceUnauthorizedError extends Schema.TaggedErrorClass()("Works
1105
1138
  message: Schema.String,
1106
1139
  }, { httpApiStatus: 401 }) {
1107
1140
  }
1141
+ /**
1142
+ * `sshAsOwner` refused: the owner is bound to no person (`ssh-owner-unbound`), the create carries no
1143
+ * capture owner map (`ssh-owner-needs-owner-map`), or the owner map or `credentialsHome` does not
1144
+ * give the owner's bound person their own uid and home (`ssh-owner-mismatch`). Names no path.
1145
+ */
1146
+ export class WorkspaceSshOwnerRefusedError extends Schema.TaggedErrorClass()("WorkspaceSshOwnerRefusedError", {
1147
+ message: Schema.String,
1148
+ code: Schema.Literals(["ssh-owner-unbound", "ssh-owner-needs-owner-map", "ssh-owner-mismatch"]),
1149
+ }, { httpApiStatus: 403 }) {
1150
+ }
1108
1151
  export class WorkspaceForbiddenError extends Schema.TaggedErrorClass()("WorkspaceForbiddenError", {
1109
1152
  message: Schema.String,
1110
1153
  }, { httpApiStatus: 403 }) {
@@ -1150,6 +1193,7 @@ export const WorkspacesGroup = HttpApiGroup.make("workspaces")
1150
1193
  WorkspaceRuntimeEnvReferencesUnsupportedError,
1151
1194
  WorkspaceDockerServiceUnsupportedError,
1152
1195
  WorkspaceForbiddenError,
1196
+ WorkspaceSshOwnerRefusedError,
1153
1197
  WorkspaceNotFoundError,
1154
1198
  // Selected connected account exists but is not usable (status "invalid").
1155
1199
  WorkspaceConflictError,
@@ -1414,6 +1458,12 @@ HttpApiEndpoint.post("expireWorkspace", "/:workspaceId/expire", {
1414
1458
  payload: renameWorkspaceRequestSchema,
1415
1459
  success: renameWorkspaceResponseSchema,
1416
1460
  error: [WorkspaceNotFoundError, WorkspaceInternalServerError],
1461
+ }))
1462
+ .add(HttpApiEndpoint.delete("clearWorkspaceSshUser", "/:workspaceId/ssh-user", {
1463
+ params: workspaceIdParams,
1464
+ query: clearWorkspaceSshUserQuerySchema,
1465
+ success: clearWorkspaceSshUserResponseSchema,
1466
+ error: [WorkspaceNotFoundError, WorkspaceInternalServerError],
1417
1467
  }))
1418
1468
  .add(HttpApiEndpoint.get("listWorkspaces", "/", {
1419
1469
  query: listWorkspacesQuerySchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealant/api-contracts-next",
3
- "version": "0.39.0-next.712",
3
+ "version": "0.39.0-next.714",
4
4
  "description": "Contract-first HTTP API definitions for Sealant services",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -32,7 +32,7 @@
32
32
  "peerDependencies": {
33
33
  "effect": ">=4.0.0-beta.85 <5"
34
34
  },
35
- "gitHead": "43bed8e5b68a8412670ea54770a7d203c6b0a554",
35
+ "gitHead": "6da21a94e3f8650df5cbdb53626f420981cd1831",
36
36
  "publishConfig": {
37
37
  "access": "public"
38
38
  }