@sealant/api-contracts-next 0.39.0-next.695 → 0.39.0-next.700

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.
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The owner map of a capture workspace (sealantd ADR-0015 "Per-person saved directories", Mend's
3
+ * ADR 0016 decision 8): who owns what the daemon's restore writes, and so whether its executor is a
4
+ * per-person one. The ONE definition of its shape, its bounds and its wire encoding, shared by the
5
+ * SDK (client-side refusal with the server's wording), the control-plane validators (every parse:
6
+ * create, worker, restart) and the runtime adapters (the boot env).
7
+ *
8
+ * The daemon reads it from its boot environment as `SEALANT_CAPTURE_OWNER_MAP`, JSON
9
+ * `{"gid":…,"worktree":…,"people":{"<id>":uid,…}}`, parsed strictly (unknown fields refused):
10
+ *
11
+ * - each listed person's saved directory (`<harness home>/people/<id>/`) is restored owned by their
12
+ * uid and the group, the directory itself 0710, its `conversations/` group-readable and -writable;
13
+ * - the worktree and its git directory are given to the group (owned by `worktreeUid`, setgid, the
14
+ * owner's bits copied to the group);
15
+ * - a root daemon whose map names at least one person does not set no-new-privileges, so every
16
+ * person's passwordless `sudo` works. A map naming nobody keeps it.
17
+ *
18
+ * Deliberately dependency-free, like `workspace-environment.ts`: data plus pure functions.
19
+ */
20
+ /** The daemon's name for the owner map in its boot environment. */
21
+ export declare const CAPTURE_OWNER_MAP_ENV = "SEALANT_CAPTURE_OWNER_MAP";
22
+ /** The shared group every person is in: Mend's `mend`, the reserved range's first id. */
23
+ export declare const CAPTURE_OWNER_MAP_GID = 40000;
24
+ /**
25
+ * The uids people may have: the reserved range (40000–49999) less its first id, the group's. The
26
+ * managed images leave the whole range free (their probe checks it).
27
+ */
28
+ export declare const CAPTURE_OWNER_MAP_UID_RANGE: {
29
+ readonly first: 40001;
30
+ readonly last: 49999;
31
+ };
32
+ /** At most this many people in one map. */
33
+ export declare const CAPTURE_OWNER_MAP_MAX_PEOPLE = 256;
34
+ /**
35
+ * A person's id: the name of their saved directory under `people/` (Mend's account id). One plain
36
+ * path component of at most 128 characters that starts with a letter, a digit or `_` (so never `.`
37
+ * or `..`).
38
+ */
39
+ export declare const CAPTURE_OWNER_MAP_ID_PATTERN: RegExp;
40
+ /** One person in an owner map. */
41
+ export interface CaptureOwnerMapPerson {
42
+ /** The name of their saved directory under `<harness home>/people/` (Mend's account id). */
43
+ readonly id: string;
44
+ /** Their Linux uid, in {@link CAPTURE_OWNER_MAP_UID_RANGE}. */
45
+ readonly uid: number;
46
+ }
47
+ /** Who owns what a capture restore writes. */
48
+ export interface CaptureOwnerMap {
49
+ /** The shared group every person is in: {@link CAPTURE_OWNER_MAP_GID}. */
50
+ readonly gid: number;
51
+ /** The change's owner: owns the worktree root and its git directory. */
52
+ readonly worktreeUid: number;
53
+ /** Each current member. A removed member is left out: their directory is restored root's. */
54
+ readonly people: ReadonlyArray<CaptureOwnerMapPerson>;
55
+ }
56
+ /**
57
+ * What is wrong with a map, in the words the control plane answers with; empty when nothing is.
58
+ * Checks the gid, every uid's range, every id's form, and that no id and no uid appears twice (one
59
+ * person's saved directory is never handed to another).
60
+ */
61
+ export declare const captureOwnerMapProblems: (map: CaptureOwnerMap) => readonly string[];
62
+ /**
63
+ * The map as the daemon parses it (`SEALANT_CAPTURE_OWNER_MAP`): compact JSON, `people` an object
64
+ * keyed by id in code-point order (the daemon's own order), always present.
65
+ */
66
+ export declare const encodeCaptureOwnerMap: (map: CaptureOwnerMap) => string;
@@ -0,0 +1,104 @@
1
+ /**
2
+ * The owner map of a capture workspace (sealantd ADR-0015 "Per-person saved directories", Mend's
3
+ * ADR 0016 decision 8): who owns what the daemon's restore writes, and so whether its executor is a
4
+ * per-person one. The ONE definition of its shape, its bounds and its wire encoding, shared by the
5
+ * SDK (client-side refusal with the server's wording), the control-plane validators (every parse:
6
+ * create, worker, restart) and the runtime adapters (the boot env).
7
+ *
8
+ * The daemon reads it from its boot environment as `SEALANT_CAPTURE_OWNER_MAP`, JSON
9
+ * `{"gid":…,"worktree":…,"people":{"<id>":uid,…}}`, parsed strictly (unknown fields refused):
10
+ *
11
+ * - each listed person's saved directory (`<harness home>/people/<id>/`) is restored owned by their
12
+ * uid and the group, the directory itself 0710, its `conversations/` group-readable and -writable;
13
+ * - the worktree and its git directory are given to the group (owned by `worktreeUid`, setgid, the
14
+ * owner's bits copied to the group);
15
+ * - a root daemon whose map names at least one person does not set no-new-privileges, so every
16
+ * person's passwordless `sudo` works. A map naming nobody keeps it.
17
+ *
18
+ * Deliberately dependency-free, like `workspace-environment.ts`: data plus pure functions.
19
+ */
20
+ /** The daemon's name for the owner map in its boot environment. */
21
+ export const CAPTURE_OWNER_MAP_ENV = "SEALANT_CAPTURE_OWNER_MAP";
22
+ /** The shared group every person is in: Mend's `mend`, the reserved range's first id. */
23
+ export const CAPTURE_OWNER_MAP_GID = 40_000;
24
+ /**
25
+ * The uids people may have: the reserved range (40000–49999) less its first id, the group's. The
26
+ * managed images leave the whole range free (their probe checks it).
27
+ */
28
+ export const CAPTURE_OWNER_MAP_UID_RANGE = { first: 40_001, last: 49_999 };
29
+ /** At most this many people in one map. */
30
+ export const CAPTURE_OWNER_MAP_MAX_PEOPLE = 256;
31
+ /**
32
+ * A person's id: the name of their saved directory under `people/` (Mend's account id). One plain
33
+ * path component of at most 128 characters that starts with a letter, a digit or `_` (so never `.`
34
+ * or `..`).
35
+ */
36
+ export const CAPTURE_OWNER_MAP_ID_PATTERN = /^[A-Za-z0-9_][A-Za-z0-9._-]{0,127}$/u;
37
+ const isUid = (value) => Number.isInteger(value) &&
38
+ value >= CAPTURE_OWNER_MAP_UID_RANGE.first &&
39
+ value <= CAPTURE_OWNER_MAP_UID_RANGE.last;
40
+ const UID_RANGE_TEXT = `${CAPTURE_OWNER_MAP_UID_RANGE.first}–${CAPTURE_OWNER_MAP_UID_RANGE.last}`;
41
+ /**
42
+ * What is wrong with a map, in the words the control plane answers with; empty when nothing is.
43
+ * Checks the gid, every uid's range, every id's form, and that no id and no uid appears twice (one
44
+ * person's saved directory is never handed to another).
45
+ */
46
+ export const captureOwnerMapProblems = (map) => {
47
+ // A plain JavaScript caller may hand anything: refused in words, never a TypeError.
48
+ const shape = map;
49
+ if (typeof shape !== "object" || shape === null) {
50
+ return ["ownerMap must be an object { gid, worktreeUid, people }"];
51
+ }
52
+ if (!Array.isArray(map.people)) {
53
+ return ["ownerMap.people must be an array of { id, uid }"];
54
+ }
55
+ const problems = [];
56
+ if (map.gid !== CAPTURE_OWNER_MAP_GID) {
57
+ problems.push(`ownerMap.gid must be ${CAPTURE_OWNER_MAP_GID} (the mend group), not ${map.gid}`);
58
+ }
59
+ if (!isUid(map.worktreeUid)) {
60
+ problems.push(`ownerMap.worktreeUid ${map.worktreeUid} is not a uid in ${UID_RANGE_TEXT}`);
61
+ }
62
+ if (map.people.length > CAPTURE_OWNER_MAP_MAX_PEOPLE) {
63
+ problems.push(`ownerMap.people has ${map.people.length} people; the maximum is ${CAPTURE_OWNER_MAP_MAX_PEOPLE}`);
64
+ return problems;
65
+ }
66
+ const ids = new Set();
67
+ const uids = new Map();
68
+ for (const [index, person] of map.people.entries()) {
69
+ const entry = person;
70
+ if (typeof entry !== "object" || entry === null) {
71
+ problems.push(`ownerMap.people[${index}] must be an object { id, uid }`);
72
+ continue;
73
+ }
74
+ if (typeof person.id !== "string" || !CAPTURE_OWNER_MAP_ID_PATTERN.test(person.id)) {
75
+ problems.push(`ownerMap.people[${index}].id must be one directory name of letters, digits, '.', '_' or '-' (at most 128, not starting with '.' or '-')`);
76
+ continue;
77
+ }
78
+ if (ids.has(person.id)) {
79
+ problems.push(`ownerMap.people names ${person.id} twice`);
80
+ }
81
+ ids.add(person.id);
82
+ if (!isUid(person.uid)) {
83
+ problems.push(`ownerMap.people ${person.id} has uid ${person.uid}, not a uid in ${UID_RANGE_TEXT}`);
84
+ continue;
85
+ }
86
+ const other = uids.get(person.uid);
87
+ if (other !== undefined && other !== person.id) {
88
+ problems.push(`ownerMap.people gives ${other} and ${person.id} the same uid ${person.uid}`);
89
+ }
90
+ uids.set(person.uid, person.id);
91
+ }
92
+ return problems;
93
+ };
94
+ /**
95
+ * The map as the daemon parses it (`SEALANT_CAPTURE_OWNER_MAP`): compact JSON, `people` an object
96
+ * keyed by id in code-point order (the daemon's own order), always present.
97
+ */
98
+ export const encodeCaptureOwnerMap = (map) => {
99
+ // `fromEntries` defines own properties, so an id such as `__proto__` stays a key.
100
+ const people = Object.fromEntries(map.people
101
+ .toSorted((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))
102
+ .map((person) => [person.id, person.uid]));
103
+ return JSON.stringify({ gid: map.gid, worktree: map.worktreeUid, people });
104
+ };
@@ -855,7 +855,52 @@ export declare const ControlPlaneAPI: HttpApi.HttpApi<"sealantControlPlaneApi",
855
855
  readonly email: import("effect/Schema").String;
856
856
  readonly name: import("effect/Schema").String;
857
857
  readonly createdAt: import("effect/Schema").String;
858
- }>>, import("effect/unstable/httpapi/HttpApiEndpoint").Json<typeof import("./users.js").UserNotFoundError | typeof import("./users.js").UserInternalServerError>, never, never>, false> | import("effect/unstable/httpapi/HttpApiGroup").HttpApiGroup<"workspaces", import("effect/unstable/httpapi/HttpApiEndpoint").HttpApiEndpoint<"bindWorkspace", "POST", "/v1/workspaces/:workspaceId/bind", import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<import("effect/Schema").Struct<{
858
+ }>>, import("effect/unstable/httpapi/HttpApiEndpoint").Json<typeof import("./users.js").UserNotFoundError | typeof import("./users.js").UserInternalServerError>, never, never>, false> | import("effect/unstable/httpapi/HttpApiGroup").HttpApiGroup<"workspaces", import("effect/unstable/httpapi/HttpApiEndpoint").HttpApiEndpoint<"applyWorkspaceDotfiles", "POST", "/v1/workspaces/:workspaceId/dotfiles", import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<import("effect/Schema").Struct<{
859
+ readonly workspaceId: import("effect/Schema").String;
860
+ }>>, import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<never>, import("effect/unstable/httpapi/HttpApiEndpoint").Json<import("effect/Schema").Struct<{
861
+ readonly ownerUserId: import("effect/Schema").String;
862
+ readonly onBehalfOfUserId: import("effect/Schema").String;
863
+ readonly user: import("effect/Schema").String;
864
+ readonly home: import("effect/Schema").String;
865
+ readonly repository: import("effect/Schema").optional<import("effect/Schema").Struct<{
866
+ readonly url: import("effect/Schema").String;
867
+ readonly ref: import("effect/Schema").optional<import("effect/Schema").String>;
868
+ readonly manager: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["auto", "chezmoi", "stow", "copy"]>>;
869
+ readonly bootstrap: import("effect/Schema").optional<import("effect/Schema").Boolean>;
870
+ readonly bootstrapCommand: import("effect/Schema").optional<import("effect/Schema").String>;
871
+ }>>;
872
+ readonly archives: import("effect/Schema").optional<import("effect/Schema").$Array<import("effect/Schema").Struct<{
873
+ readonly data: import("effect/Schema").String;
874
+ readonly manager: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["auto", "chezmoi", "stow", "copy"]>>;
875
+ readonly target: import("effect/Schema").optional<import("effect/Schema").Literals<readonly ["home", "config"]>>;
876
+ readonly bootstrap: import("effect/Schema").optional<import("effect/Schema").Boolean>;
877
+ readonly bootstrapCommand: import("effect/Schema").optional<import("effect/Schema").String>;
878
+ }>>>;
879
+ }>>, import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<never>, import("effect/unstable/httpapi/HttpApiEndpoint").Json<import("effect/Schema").Struct<{
880
+ readonly runId: import("effect/Schema").String;
881
+ readonly workspaceId: import("effect/Schema").String;
882
+ readonly attemptId: import("effect/Schema").optional<import("effect/Schema").String>;
883
+ readonly ownerUserId: import("effect/Schema").String;
884
+ readonly harnessId: import("effect/Schema").String;
885
+ readonly mode: import("effect/Schema").Literals<readonly ["one-shot", "interactive"]>;
886
+ readonly status: import("effect/Schema").Literals<readonly ["queued", "running", "completed", "failed", "cancelled"]>;
887
+ readonly prompt: import("effect/Schema").optional<import("effect/Schema").String>;
888
+ readonly command: import("effect/Schema").optional<import("effect/Schema").Struct<{
889
+ readonly executable: import("effect/Schema").String;
890
+ readonly args: import("effect/Schema").$Array<import("effect/Schema").String>;
891
+ readonly cwd: import("effect/Schema").optional<import("effect/Schema").String>;
892
+ readonly argCount: import("effect/Schema").optional<import("effect/Schema").Number>;
893
+ readonly argLengths: import("effect/Schema").optional<import("effect/Schema").$Array<import("effect/Schema").Number>>;
894
+ }>>;
895
+ readonly metadata: import("effect/Schema").optional<import("effect/Schema").$Record<import("effect/Schema").String, import("effect/Schema").Unknown>>;
896
+ readonly exitCode: import("effect/Schema").optional<import("effect/Schema").Number>;
897
+ readonly errorMessage: import("effect/Schema").optional<import("effect/Schema").String>;
898
+ readonly startedAt: import("effect/Schema").optional<import("effect/Schema").String>;
899
+ readonly finishedAt: import("effect/Schema").optional<import("effect/Schema").String>;
900
+ readonly recordDeletedAt: import("effect/Schema").optional<import("effect/Schema").String>;
901
+ readonly createdAt: import("effect/Schema").String;
902
+ readonly updatedAt: import("effect/Schema").String;
903
+ }>>, import("effect/unstable/httpapi/HttpApiEndpoint").Json<typeof import("./workspaces.js").WorkspaceBadRequestError | typeof import("./workspaces.js").WorkspaceForbiddenError | typeof import("./workspaces.js").WorkspaceNotFoundError | typeof import("./workspaces.js").WorkspaceConflictError | typeof import("./workspaces.js").WorkspaceBadGatewayError | typeof import("./workspaces.js").WorkspaceServiceUnavailableError | typeof import("./workspaces.js").WorkspaceInternalServerError>, never, never> | import("effect/unstable/httpapi/HttpApiEndpoint").HttpApiEndpoint<"bindWorkspace", "POST", "/v1/workspaces/:workspaceId/bind", import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<import("effect/Schema").Struct<{
859
904
  readonly workspaceId: import("effect/Schema").String;
860
905
  }>>, import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<never>, import("effect/unstable/httpapi/HttpApiEndpoint").Json<import("effect/Schema").Struct<{
861
906
  readonly ownerUserId: import("effect/Schema").String;
@@ -1026,6 +1071,7 @@ export declare const ControlPlaneAPI: HttpApi.HttpApi<"sealantControlPlaneApi",
1026
1071
  readonly runningMs: import("effect/Schema").Number;
1027
1072
  readonly boundMs: import("effect/Schema").Number;
1028
1073
  }>>;
1074
+ readonly ownerMap: import("effect/Schema").optional<import("effect/Schema").Boolean>;
1029
1075
  }>>, import("effect/unstable/httpapi/HttpApiEndpoint").Json<typeof import("./workspaces.js").WorkspaceBadRequestError | typeof import("./workspaces.js").WorkspaceNotFoundError | typeof import("./workspaces.js").WorkspaceConflictError | typeof import("./workspaces.js").WorkspaceInternalServerError>, never, never> | import("effect/unstable/httpapi/HttpApiEndpoint").HttpApiEndpoint<"getWorkspace", "GET", "/v1/workspaces/:workspaceId", import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<import("effect/Schema").Struct<{
1030
1076
  readonly workspaceId: import("effect/Schema").String;
1031
1077
  }>>, import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<import("effect/Schema").Struct<{
@@ -1164,6 +1210,7 @@ export declare const ControlPlaneAPI: HttpApi.HttpApi<"sealantControlPlaneApi",
1164
1210
  readonly runningMs: import("effect/Schema").Number;
1165
1211
  readonly boundMs: import("effect/Schema").Number;
1166
1212
  }>>;
1213
+ readonly ownerMap: import("effect/Schema").optional<import("effect/Schema").Boolean>;
1167
1214
  }>>, import("effect/unstable/httpapi/HttpApiEndpoint").Json<typeof import("./workspaces.js").WorkspaceBadRequestError | typeof import("./workspaces.js").WorkspaceNotFoundError | typeof import("./workspaces.js").WorkspaceConflictError | typeof import("./workspaces.js").WorkspaceInternalServerError>, never, never> | import("effect/unstable/httpapi/HttpApiEndpoint").HttpApiEndpoint<"getWorkspaceCreate", "GET", "/v1/workspaces/idempotency-keys/:idempotencyKey", import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<import("effect/Schema").Struct<{
1168
1215
  readonly idempotencyKey: import("effect/Schema").String;
1169
1216
  }>>, import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<import("effect/Schema").Struct<{
@@ -1284,6 +1331,14 @@ export declare const ControlPlaneAPI: HttpApi.HttpApi<"sealantControlPlaneApi",
1284
1331
  readonly connectedAccountId: import("effect/Schema").String;
1285
1332
  readonly name: import("effect/Schema").String;
1286
1333
  }>>;
1334
+ readonly pi: import("effect/Schema").optional<import("effect/Schema").Struct<{
1335
+ readonly connectedAccountId: import("effect/Schema").String;
1336
+ readonly name: import("effect/Schema").String;
1337
+ }>>;
1338
+ readonly opencode: import("effect/Schema").optional<import("effect/Schema").Struct<{
1339
+ readonly connectedAccountId: import("effect/Schema").String;
1340
+ readonly name: import("effect/Schema").String;
1341
+ }>>;
1287
1342
  }>;
1288
1343
  }>>;
1289
1344
  }>>, import("effect/unstable/httpapi/HttpApiEndpoint").Json<typeof import("./workspaces.js").WorkspaceForbiddenError | typeof import("./workspaces.js").WorkspaceNotFoundError | typeof import("./workspaces.js").WorkspaceConflictError | typeof import("./workspaces.js").WorkspaceInternalServerError>, never, never> | import("effect/unstable/httpapi/HttpApiEndpoint").HttpApiEndpoint<"listWorkspaceEvents", "GET", "/v1/workspaces/:workspaceId/events", import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<import("effect/Schema").Struct<{
@@ -1358,6 +1413,9 @@ export declare const ControlPlaneAPI: HttpApi.HttpApi<"sealantControlPlaneApi",
1358
1413
  readonly claude: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").String>>;
1359
1414
  readonly codex: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").String>>;
1360
1415
  readonly github: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").String>>;
1416
+ readonly pi: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").String>>;
1417
+ readonly opencode: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").String>>;
1418
+ readonly partial: import("effect/Schema").optional<import("effect/Schema").Boolean>;
1361
1419
  }>>, import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<never>, import("effect/unstable/httpapi/HttpApiEndpoint").Json<import("effect/Schema").Struct<{
1362
1420
  readonly workspaceId: import("effect/Schema").String;
1363
1421
  readonly runId: import("effect/Schema").String;
@@ -1377,8 +1435,21 @@ export declare const ControlPlaneAPI: HttpApi.HttpApi<"sealantControlPlaneApi",
1377
1435
  readonly connectedAccountId: import("effect/Schema").String;
1378
1436
  readonly name: import("effect/Schema").String;
1379
1437
  }>>;
1438
+ readonly pi: import("effect/Schema").optional<import("effect/Schema").Struct<{
1439
+ readonly connectedAccountId: import("effect/Schema").String;
1440
+ readonly name: import("effect/Schema").String;
1441
+ }>>;
1442
+ readonly opencode: import("effect/Schema").optional<import("effect/Schema").Struct<{
1443
+ readonly connectedAccountId: import("effect/Schema").String;
1444
+ readonly name: import("effect/Schema").String;
1445
+ }>>;
1380
1446
  }>;
1381
1447
  }>;
1448
+ readonly skipped: import("effect/Schema").optional<import("effect/Schema").$Array<import("effect/Schema").Struct<{
1449
+ readonly provider: import("effect/Schema").Literals<readonly ["claude", "codex", "github", "pi", "opencode"]>;
1450
+ readonly code: import("effect/Schema").Literals<readonly ["connected-account-missing", "connected-account-invalid", "connected-account-unsupported", "login-file-unusable"]>;
1451
+ readonly message: import("effect/Schema").String;
1452
+ }>>>;
1382
1453
  }>>, import("effect/unstable/httpapi/HttpApiEndpoint").Json<typeof import("./workspaces.js").WorkspaceBadRequestError | typeof import("./workspaces.js").WorkspaceForbiddenError | typeof import("./workspaces.js").WorkspaceNotFoundError | typeof import("./workspaces.js").WorkspaceConflictError | typeof import("./workspaces.js").WorkspaceBadGatewayError | typeof import("./workspaces.js").WorkspaceServiceUnavailableError | typeof import("./workspaces.js").WorkspaceInternalServerError>, never, never> | import("effect/unstable/httpapi/HttpApiEndpoint").HttpApiEndpoint<"recoverWorkspace", "POST", "/v1/workspaces/:workspaceId/recover", import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<import("effect/Schema").Struct<{
1383
1454
  readonly workspaceId: import("effect/Schema").String;
1384
1455
  }>>, import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<never>, import("effect/unstable/httpapi/HttpApiEndpoint").Json<import("effect/Schema").Struct<{
@@ -1410,6 +1481,14 @@ export declare const ControlPlaneAPI: HttpApi.HttpApi<"sealantControlPlaneApi",
1410
1481
  readonly workspaceId: import("effect/Schema").String;
1411
1482
  }>>, import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<never>, import("effect/unstable/httpapi/HttpApiEndpoint").Json<import("effect/Schema").Struct<{
1412
1483
  readonly ownerUserId: import("effect/Schema").String;
1484
+ readonly expectedOwnerMap: import("effect/Schema").optional<import("effect/Schema").NullOr<import("effect/Schema").Struct<{
1485
+ readonly gid: import("effect/Schema").Number;
1486
+ readonly worktreeUid: import("effect/Schema").Number;
1487
+ readonly people: import("effect/Schema").$Array<import("effect/Schema").Struct<{
1488
+ readonly id: import("effect/Schema").String;
1489
+ readonly uid: import("effect/Schema").Number;
1490
+ }>>;
1491
+ }>>>;
1413
1492
  }>>, import("effect/unstable/httpapi/HttpApiEndpoint").StringTree<never>, import("effect/unstable/httpapi/HttpApiEndpoint").Json<import("effect/Schema").Struct<{
1414
1493
  readonly worktreeId: import("effect/Schema").String;
1415
1494
  readonly epoch: import("effect/Schema").Number;
@@ -186,6 +186,95 @@ export declare const execWorkspaceRequestSchema: Schema.Struct<{
186
186
  readonly user: Schema.optional<Schema.String>;
187
187
  }>;
188
188
  export type ExecWorkspaceRequest = typeof execWorkspaceRequestSchema.Type;
189
+ /** The `harnessId` stamped on runs that apply a person's dotfiles (`applyWorkspaceDotfiles`). */
190
+ export declare const dotfilesRunHarnessId = "dotfiles";
191
+ /** How a dotfiles tree is applied: chezmoi and stow layouts are detected by `auto`. */
192
+ export declare const workspaceDotfilesManagerSchema: Schema.Literals<readonly ["auto", "chezmoi", "stow", "copy"]>;
193
+ /**
194
+ * A dotfiles repository cloned as the person (no credential reaches the workspace: a repository
195
+ * only the person's own identity can reach is resolved by the caller and sent as an archive).
196
+ */
197
+ export declare const applyWorkspaceDotfilesRepositorySchema: Schema.Struct<{
198
+ /** An `https://` clone URL with no credential in it (no `user:token@`). */
199
+ readonly url: Schema.String;
200
+ /** Branch or tag; omitted clones the remote's default branch. */
201
+ readonly ref: Schema.optional<Schema.String>;
202
+ readonly manager: Schema.optional<Schema.Literals<readonly ["auto", "chezmoi", "stow", "copy"]>>;
203
+ /** Run the bootstrap command after applying (when the checkout has it). Default true. */
204
+ readonly bootstrap: Schema.optional<Schema.Boolean>;
205
+ /** Relative to the checkout; default `./install.sh`. */
206
+ readonly bootstrapCommand: Schema.optional<Schema.String>;
207
+ }>;
208
+ /** A caller-resolved dotfiles tree, as at create (`spec.runtime.dotfilesArchives`). */
209
+ export declare const applyWorkspaceDotfilesArchiveSchema: Schema.Struct<{
210
+ /** base64 of a `.tar.gz`; at most 6 MiB of base64 (about 4 MiB decoded). */
211
+ readonly data: Schema.String;
212
+ readonly manager: Schema.optional<Schema.Literals<readonly ["auto", "chezmoi", "stow", "copy"]>>;
213
+ /** Where the tree lands: `home` (default) or `config` (`~/.config`, copy manager only). */
214
+ readonly target: Schema.optional<Schema.Literals<readonly ["home", "config"]>>;
215
+ /** Run `./install.sh` (or `bootstrapCommand`) after applying when present. Default true. */
216
+ readonly bootstrap: Schema.optional<Schema.Boolean>;
217
+ readonly bootstrapCommand: Schema.optional<Schema.String>;
218
+ }>;
219
+ /**
220
+ * Apply a person's dotfiles into their home of a RUNNING workspace, as their Linux user
221
+ * (docs/connected-accounts-design.md §6g; Mend's ADR 0016 decision 11): the same sources as a
222
+ * create's dotfiles (a repository, archives, or both; the repository first, archives after in
223
+ * order) and the same applier (sealantd's: chezmoi, stow or copy, then each tree's bootstrap).
224
+ *
225
+ * `user` must exist and must not be root or in root's group, and `home` must be its passwd home: an
226
+ * existing directory of the user's, reached without a symbolic link. The clone, chezmoi, stow and the
227
+ * bootstrap run as the user; sealantd's archive staging and `copy` manager still run as root inside
228
+ * the home and follow links the person planted there, until sealantd's fix for it lands (see
229
+ * docs/connected-accounts-design.md §6g). `onBehalfOfUserId` names whose dotfiles they are: it is
230
+ * recorded on the run, and a home whose logins another person holds is refused (`home-held`). Answered `202` with a run (`harnessId`
231
+ * `dotfiles`) once the archives are staged: the run's first `processStarted` is the bootstrap,
232
+ * started once every file is applied; its output and exit are the run's. The run completes with the
233
+ * bootstrap's exit code (0 when no tree had one) and fails, with the daemon's words, when the apply
234
+ * was refused or the bootstrap ran past 30 minutes (it is stopped).
235
+ */
236
+ export declare const applyWorkspaceDotfilesRequestSchema: Schema.Struct<{
237
+ /** The workspace's owner (owner scope; uniform 404 otherwise). */
238
+ readonly ownerUserId: Schema.String;
239
+ /** The Sealant user whose dotfiles these are: recorded on the run (`metadata.dotfiles`). */
240
+ readonly onBehalfOfUserId: Schema.String;
241
+ /** The Linux user to apply as (a login name or a uid); never root. */
242
+ readonly user: Schema.String;
243
+ /** The user's passwd home: absolute, normalised, never under `/workspace`. */
244
+ readonly home: Schema.String;
245
+ readonly repository: Schema.optional<Schema.Struct<{
246
+ /** An `https://` clone URL with no credential in it (no `user:token@`). */
247
+ readonly url: Schema.String;
248
+ /** Branch or tag; omitted clones the remote's default branch. */
249
+ readonly ref: Schema.optional<Schema.String>;
250
+ readonly manager: Schema.optional<Schema.Literals<readonly ["auto", "chezmoi", "stow", "copy"]>>;
251
+ /** Run the bootstrap command after applying (when the checkout has it). Default true. */
252
+ readonly bootstrap: Schema.optional<Schema.Boolean>;
253
+ /** Relative to the checkout; default `./install.sh`. */
254
+ readonly bootstrapCommand: Schema.optional<Schema.String>;
255
+ }>>;
256
+ readonly archives: Schema.optional<Schema.$Array<Schema.Struct<{
257
+ /** base64 of a `.tar.gz`; at most 6 MiB of base64 (about 4 MiB decoded). */
258
+ readonly data: Schema.String;
259
+ readonly manager: Schema.optional<Schema.Literals<readonly ["auto", "chezmoi", "stow", "copy"]>>;
260
+ /** Where the tree lands: `home` (default) or `config` (`~/.config`, copy manager only). */
261
+ readonly target: Schema.optional<Schema.Literals<readonly ["home", "config"]>>;
262
+ /** Run `./install.sh` (or `bootstrapCommand`) after applying when present. Default true. */
263
+ readonly bootstrap: Schema.optional<Schema.Boolean>;
264
+ readonly bootstrapCommand: Schema.optional<Schema.String>;
265
+ }>>>;
266
+ }>;
267
+ export type ApplyWorkspaceDotfilesRequest = typeof applyWorkspaceDotfilesRequestSchema.Type;
268
+ /**
269
+ * Stable `code`s of the `WorkspaceConflictError` a dotfiles apply answers, nothing applied:
270
+ * `workspace-not-running` (no ready executor), `dotfiles-user-unsupported` (the workspace's
271
+ * sealantd cannot apply dotfiles as a user), `user-unknown` (no such user: make it first),
272
+ * `user-root` (the user is root or in root's group), `home-mismatch` (the home is not the user's
273
+ * passwd home), `home-unusable` (the home is missing, not a directory, not the user's, or reached
274
+ * through a symbolic link), `home-held` (another person's logins are held in the home).
275
+ */
276
+ export declare const workspaceDotfilesConflictCodes: readonly ["workspace-not-running", "dotfiles-user-unsupported", "user-unknown", "user-root", "home-mismatch", "home-unusable", "home-held"];
277
+ export type WorkspaceDotfilesConflictCode = (typeof workspaceDotfilesConflictCodes)[number];
189
278
  /**
190
279
  * Bind a standby workspace's working directory, or a bindable extra mount, to one subdirectory of
191
280
  * its root (sealantd ADR-0014). `mountPath` defaults to the working directory; an empty `subpath`
@@ -406,6 +495,12 @@ export declare const workspaceCaptureStatusSchema: Schema.Struct<{
406
495
  readonly runningMs: Schema.Number;
407
496
  readonly boundMs: Schema.Number;
408
497
  }>>;
498
+ /**
499
+ * The executor booted under an owner map (`SEALANT_CAPTURE_OWNER_MAP`): its restores give each
500
+ * mapped person's saved directory to them and the worktree to the group. Absent from a control
501
+ * plane that predates it; false from a daemon that predates it.
502
+ */
503
+ readonly ownerMap: Schema.optional<Schema.Boolean>;
409
504
  }>;
410
505
  export type WorkspaceCaptureStatus = typeof workspaceCaptureStatusSchema.Type;
411
506
  /**
@@ -417,6 +512,21 @@ export type WorkspaceCaptureStatus = typeof workspaceCaptureStatusSchema.Type;
417
512
  */
418
513
  export declare const replanWorkspaceCaptureRequestSchema: Schema.Struct<{
419
514
  readonly ownerUserId: Schema.String;
515
+ /**
516
+ * The owner map the claim needs (a capture source's `ownerMap`), or `null` for none. The daemon
517
+ * reads its map only at boot, so a re-plan restores under the map the executor was launched
518
+ * with: when this is given and differs from that map (including a standby launched with none),
519
+ * the re-plan is refused with `409` code `owner-map-mismatch` before the daemon is reached.
520
+ * Omitted, nothing is compared.
521
+ */
522
+ readonly expectedOwnerMap: Schema.optional<Schema.NullOr<Schema.Struct<{
523
+ readonly gid: Schema.Number;
524
+ readonly worktreeUid: Schema.Number;
525
+ readonly people: Schema.$Array<Schema.Struct<{
526
+ readonly id: Schema.String;
527
+ readonly uid: Schema.Number;
528
+ }>>;
529
+ }>>>;
420
530
  }>;
421
531
  export type ReplanWorkspaceCaptureRequest = typeof replanWorkspaceCaptureRequestSchema.Type;
422
532
  export declare const workspaceCaptureReplannedSchema: Schema.Struct<{
@@ -597,9 +707,26 @@ export declare const inspectWorkspaceImageResponseSchema: Schema.Struct<{
597
707
  }>;
598
708
  }>;
599
709
  export type InspectWorkspaceImageResponse = typeof inspectWorkspaceImageResponseSchema.Type;
600
- /** The providers a home holds logins for (docs/connected-accounts-design.md §6c). */
601
- export declare const workspaceCredentialHomeProviders: readonly ["claude", "codex", "github"];
710
+ /**
711
+ * Stable codes of a refused connected account, carried with the account's `provider` by the error
712
+ * that refuses it (a create's or a credentials put's): `connected-account-missing`
713
+ * (`WorkspaceNotFoundError`, 404: the person has no such account, or it is archived or someone
714
+ * else's) and `connected-account-invalid` (`WorkspaceConflictError`, 409: the account is marked
715
+ * invalid, or holds an unusable credential; reconnect it) and `connected-account-unsupported`
716
+ * (`WorkspaceConflictError`, 409: the account cannot serve what was asked, as a Codex API key cannot
717
+ * be pi's or opencode's ChatGPT login). Nothing was written either way.
718
+ */
719
+ export declare const connectedAccountRefusalCodes: readonly ["connected-account-missing", "connected-account-invalid", "connected-account-unsupported"];
720
+ export type ConnectedAccountRefusalCode = (typeof connectedAccountRefusalCodes)[number];
721
+ /**
722
+ * The providers a home holds logins for (docs/connected-accounts-design.md §6c). `pi` and
723
+ * `opencode` are the person's ChatGPT login (a Codex connected account), written into each tool's
724
+ * own `auth.json`.
725
+ */
726
+ export declare const workspaceCredentialHomeProviders: readonly ["claude", "codex", "github", "pi", "opencode"];
602
727
  export type WorkspaceCredentialHomeProvider = (typeof workspaceCredentialHomeProviders)[number];
728
+ /** The connected account's provider each home provider's login is made from. */
729
+ export declare const workspaceCredentialHomeAccountProvider: Readonly<Record<WorkspaceCredentialHomeProvider, "claude" | "codex" | "github">>;
603
730
  /**
604
731
  * Put one person's logins into one home of a RUNNING workspace (docs/connected-accounts-design.md
605
732
  * §6c). A home holds one person's logins while it is held: the first put names its person, a put
@@ -631,6 +758,25 @@ export declare const putWorkspaceCredentialsRequestSchema: Schema.Struct<{
631
758
  readonly claude: Schema.optional<Schema.NullOr<Schema.String>>;
632
759
  readonly codex: Schema.optional<Schema.NullOr<Schema.String>>;
633
760
  readonly github: Schema.optional<Schema.NullOr<Schema.String>>;
761
+ /**
762
+ * pi's ChatGPT login, made from a Codex account of `onBehalfOfUserId` (an id or a Codex account
763
+ * name), as the `openai-codex` entry of `<home>/.pi/agent/auth.json`. The file is merged: its
764
+ * other entries stay, and an `openai-codex` login the person made in pi is never replaced.
765
+ */
766
+ readonly pi: Schema.optional<Schema.NullOr<Schema.String>>;
767
+ /**
768
+ * opencode's ChatGPT login, made the same way, as the `openai` entry of opencode's `auth.json`
769
+ * (`<home>/.local/share/opencode/auth.json`, followed through links to where it really is, which
770
+ * must be inside the home). A login the person made in opencode is never replaced or removed.
771
+ */
772
+ readonly opencode: Schema.optional<Schema.NullOr<Schema.String>>;
773
+ /**
774
+ * Write what the person has connected and leave out the rest: a provider whose account is refused
775
+ * (`connected-account-missing`, `-invalid` or `-unsupported`) is not written, its login is removed
776
+ * from the home as `null` would remove it, and the answer's `skipped` says which and why, instead
777
+ * of the whole put failing. Every other refusal still fails the put. Default false.
778
+ */
779
+ readonly partial: Schema.optional<Schema.Boolean>;
634
780
  }>;
635
781
  export type PutWorkspaceCredentialsRequest = typeof putWorkspaceCredentialsRequestSchema.Type;
636
782
  /** One account whose copy a home holds. */
@@ -662,9 +808,33 @@ export declare const workspaceCredentialHomeSchema: Schema.Struct<{
662
808
  /** The account's name under its provider, as it is now. */
663
809
  readonly name: Schema.String;
664
810
  }>>;
811
+ /** The Codex account pi's ChatGPT login is made from. */
812
+ readonly pi: Schema.optional<Schema.Struct<{
813
+ readonly connectedAccountId: Schema.String;
814
+ /** The account's name under its provider, as it is now. */
815
+ readonly name: Schema.String;
816
+ }>>;
817
+ /** The Codex account opencode's ChatGPT login is made from. */
818
+ readonly opencode: Schema.optional<Schema.Struct<{
819
+ readonly connectedAccountId: Schema.String;
820
+ /** The account's name under its provider, as it is now. */
821
+ readonly name: Schema.String;
822
+ }>>;
665
823
  }>;
666
824
  }>;
667
825
  export type WorkspaceCredentialHome = typeof workspaceCredentialHomeSchema.Type;
826
+ /** A provider a partial put left out, and why (its account's refusal). */
827
+ export declare const workspaceCredentialSkipSchema: Schema.Struct<{
828
+ readonly provider: Schema.Literals<readonly ["claude", "codex", "github", "pi", "opencode"]>;
829
+ /**
830
+ * The account's refusal, or `login-file-unusable`: pi's or opencode's `auth.json` in the home
831
+ * cannot be written there (not a regular file, or really outside the home).
832
+ */
833
+ readonly code: Schema.Literals<readonly ["connected-account-missing", "connected-account-invalid", "connected-account-unsupported", "login-file-unusable"]>;
834
+ /** The refusal's words, as a whole put would have answered them. */
835
+ readonly message: Schema.String;
836
+ }>;
837
+ export type WorkspaceCredentialSkip = typeof workspaceCredentialSkipSchema.Type;
668
838
  export declare const putWorkspaceCredentialsResponseSchema: Schema.Struct<{
669
839
  readonly workspaceId: Schema.String;
670
840
  /** The launch attempt whose executor was written. */
@@ -691,8 +861,31 @@ export declare const putWorkspaceCredentialsResponseSchema: Schema.Struct<{
691
861
  /** The account's name under its provider, as it is now. */
692
862
  readonly name: Schema.String;
693
863
  }>>;
864
+ /** The Codex account pi's ChatGPT login is made from. */
865
+ readonly pi: Schema.optional<Schema.Struct<{
866
+ readonly connectedAccountId: Schema.String;
867
+ /** The account's name under its provider, as it is now. */
868
+ readonly name: Schema.String;
869
+ }>>;
870
+ /** The Codex account opencode's ChatGPT login is made from. */
871
+ readonly opencode: Schema.optional<Schema.Struct<{
872
+ readonly connectedAccountId: Schema.String;
873
+ /** The account's name under its provider, as it is now. */
874
+ readonly name: Schema.String;
875
+ }>>;
694
876
  }>;
695
877
  }>;
878
+ /** A partial put's left-out providers, in the order named (empty when all were written). */
879
+ readonly skipped: Schema.optional<Schema.$Array<Schema.Struct<{
880
+ readonly provider: Schema.Literals<readonly ["claude", "codex", "github", "pi", "opencode"]>;
881
+ /**
882
+ * The account's refusal, or `login-file-unusable`: pi's or opencode's `auth.json` in the home
883
+ * cannot be written there (not a regular file, or really outside the home).
884
+ */
885
+ readonly code: Schema.Literals<readonly ["connected-account-missing", "connected-account-invalid", "connected-account-unsupported", "login-file-unusable"]>;
886
+ /** The refusal's words, as a whole put would have answered them. */
887
+ readonly message: Schema.String;
888
+ }>>>;
696
889
  }>;
697
890
  export type PutWorkspaceCredentialsResponse = typeof putWorkspaceCredentialsResponseSchema.Type;
698
891
  /** Release one home: its login files are removed and its record deleted. */
@@ -738,6 +931,18 @@ export declare const listWorkspaceCredentialsResponseSchema: Schema.Struct<{
738
931
  /** The account's name under its provider, as it is now. */
739
932
  readonly name: Schema.String;
740
933
  }>>;
934
+ /** The Codex account pi's ChatGPT login is made from. */
935
+ readonly pi: Schema.optional<Schema.Struct<{
936
+ readonly connectedAccountId: Schema.String;
937
+ /** The account's name under its provider, as it is now. */
938
+ readonly name: Schema.String;
939
+ }>>;
940
+ /** The Codex account opencode's ChatGPT login is made from. */
941
+ readonly opencode: Schema.optional<Schema.Struct<{
942
+ readonly connectedAccountId: Schema.String;
943
+ /** The account's name under its provider, as it is now. */
944
+ readonly name: Schema.String;
945
+ }>>;
741
946
  }>;
742
947
  }>>;
743
948
  }>;
@@ -745,13 +950,15 @@ export type ListWorkspaceCredentialsResponse = typeof listWorkspaceCredentialsRe
745
950
  /**
746
951
  * Stable `code`s of the `WorkspaceConflictError` the credential routes answer:
747
952
  * `workspace-not-running` (no ready executor), `connected-account-invalid` (a named account is
748
- * marked invalid: reconnect it), `home-held` (the home holds another person's logins: release it
953
+ * marked invalid or unusable: reconnect it; `provider` names its provider),
954
+ * `connected-account-unsupported` (a Codex account named for pi or opencode is not a ChatGPT
955
+ * login), `home-held` (the home holds another person's logins: release it
749
956
  * first; `/root` while the launch's own logins are at `$HOME`, or for anyone but the workspace's
750
957
  * owner), `home-unusable` (the home does not exist, is not a directory, is reached through a
751
958
  * symbolic link, or a login directory in it links outside it), `home-busy` (another write into the
752
959
  * home, or too many into this workspace, is still running: nothing was done, try again).
753
960
  */
754
- export declare const workspaceCredentialsConflictCodes: readonly ["workspace-not-running", "connected-account-invalid", "home-held", "home-unusable", "home-busy"];
961
+ export declare const workspaceCredentialsConflictCodes: readonly ["workspace-not-running", "connected-account-invalid", "connected-account-unsupported", "home-held", "home-unusable", "home-busy"];
755
962
  export type WorkspaceCredentialsConflictCode = (typeof workspaceCredentialsConflictCodes)[number];
756
963
  export declare const expireWorkspaceRequestSchema: Schema.Struct<{
757
964
  readonly ownerUserId: Schema.String;
@@ -1398,6 +1605,10 @@ export declare class WorkspaceForbiddenError extends WorkspaceForbiddenError_bas
1398
1605
  }
1399
1606
  declare const WorkspaceNotFoundError_base: Schema.Class<WorkspaceNotFoundError, Schema.TaggedStruct<"WorkspaceNotFoundError", {
1400
1607
  readonly message: Schema.String;
1608
+ /** A stable reason, where one applies (`connected-account-missing`). */
1609
+ readonly code: Schema.optional<Schema.String>;
1610
+ /** The provider of the connected account the reason is about. */
1611
+ readonly provider: Schema.optional<Schema.Literals<readonly ["claude", "codex", "github"]>>;
1401
1612
  }>, import("effect/Cause").YieldableError>;
1402
1613
  export declare class WorkspaceNotFoundError extends WorkspaceNotFoundError_base {
1403
1614
  }
@@ -1405,6 +1616,8 @@ declare const WorkspaceConflictError_base: Schema.Class<WorkspaceConflictError,
1405
1616
  readonly message: Schema.String;
1406
1617
  /** A stable reason, where one applies (`create-cancelled`: the create's key was cancelled). */
1407
1618
  readonly code: Schema.optional<Schema.String>;
1619
+ /** The provider of the connected account the reason is about (`connected-account-invalid`). */
1620
+ readonly provider: Schema.optional<Schema.Literals<readonly ["claude", "codex", "github"]>>;
1408
1621
  }>, import("effect/Cause").YieldableError>;
1409
1622
  export declare class WorkspaceConflictError extends WorkspaceConflictError_base {
1410
1623
  }
@@ -1423,7 +1636,63 @@ declare const WorkspaceInternalServerError_base: Schema.Class<WorkspaceInternalS
1423
1636
  }>, import("effect/Cause").YieldableError>;
1424
1637
  export declare class WorkspaceInternalServerError extends WorkspaceInternalServerError_base {
1425
1638
  }
1426
- export declare const WorkspacesGroup: HttpApiGroup.HttpApiGroup<"workspaces", HttpApiEndpoint.HttpApiEndpoint<"bindWorkspace", "POST", "/:workspaceId/bind", HttpApiEndpoint.StringTree<Schema.Struct<{
1639
+ export declare const WorkspacesGroup: HttpApiGroup.HttpApiGroup<"workspaces", HttpApiEndpoint.HttpApiEndpoint<"applyWorkspaceDotfiles", "POST", "/:workspaceId/dotfiles", HttpApiEndpoint.StringTree<Schema.Struct<{
1640
+ readonly workspaceId: Schema.String;
1641
+ }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
1642
+ /** The workspace's owner (owner scope; uniform 404 otherwise). */
1643
+ readonly ownerUserId: Schema.String;
1644
+ /** The Sealant user whose dotfiles these are: recorded on the run (`metadata.dotfiles`). */
1645
+ readonly onBehalfOfUserId: Schema.String;
1646
+ /** The Linux user to apply as (a login name or a uid); never root. */
1647
+ readonly user: Schema.String;
1648
+ /** The user's passwd home: absolute, normalised, never under `/workspace`. */
1649
+ readonly home: Schema.String;
1650
+ readonly repository: Schema.optional<Schema.Struct<{
1651
+ /** An `https://` clone URL with no credential in it (no `user:token@`). */
1652
+ readonly url: Schema.String;
1653
+ /** Branch or tag; omitted clones the remote's default branch. */
1654
+ readonly ref: Schema.optional<Schema.String>;
1655
+ readonly manager: Schema.optional<Schema.Literals<readonly ["auto", "chezmoi", "stow", "copy"]>>;
1656
+ /** Run the bootstrap command after applying (when the checkout has it). Default true. */
1657
+ readonly bootstrap: Schema.optional<Schema.Boolean>;
1658
+ /** Relative to the checkout; default `./install.sh`. */
1659
+ readonly bootstrapCommand: Schema.optional<Schema.String>;
1660
+ }>>;
1661
+ readonly archives: Schema.optional<Schema.$Array<Schema.Struct<{
1662
+ /** base64 of a `.tar.gz`; at most 6 MiB of base64 (about 4 MiB decoded). */
1663
+ readonly data: Schema.String;
1664
+ readonly manager: Schema.optional<Schema.Literals<readonly ["auto", "chezmoi", "stow", "copy"]>>;
1665
+ /** Where the tree lands: `home` (default) or `config` (`~/.config`, copy manager only). */
1666
+ readonly target: Schema.optional<Schema.Literals<readonly ["home", "config"]>>;
1667
+ /** Run `./install.sh` (or `bootstrapCommand`) after applying when present. Default true. */
1668
+ readonly bootstrap: Schema.optional<Schema.Boolean>;
1669
+ readonly bootstrapCommand: Schema.optional<Schema.String>;
1670
+ }>>>;
1671
+ }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
1672
+ readonly runId: Schema.String;
1673
+ readonly workspaceId: Schema.String;
1674
+ readonly attemptId: Schema.optional<Schema.String>;
1675
+ readonly ownerUserId: Schema.String;
1676
+ readonly harnessId: Schema.String;
1677
+ readonly mode: Schema.Literals<readonly ["one-shot", "interactive"]>;
1678
+ readonly status: Schema.Literals<readonly ["queued", "running", "completed", "failed", "cancelled"]>;
1679
+ readonly prompt: Schema.optional<Schema.String>;
1680
+ readonly command: Schema.optional<Schema.Struct<{
1681
+ readonly executable: Schema.String;
1682
+ readonly args: Schema.$Array<Schema.String>;
1683
+ readonly cwd: Schema.optional<Schema.String>;
1684
+ readonly argCount: Schema.optional<Schema.Number>;
1685
+ readonly argLengths: Schema.optional<Schema.$Array<Schema.Number>>;
1686
+ }>>;
1687
+ readonly metadata: Schema.optional<Schema.$Record<Schema.String, Schema.Unknown>>;
1688
+ readonly exitCode: Schema.optional<Schema.Number>;
1689
+ readonly errorMessage: Schema.optional<Schema.String>;
1690
+ readonly startedAt: Schema.optional<Schema.String>;
1691
+ readonly finishedAt: Schema.optional<Schema.String>;
1692
+ readonly recordDeletedAt: Schema.optional<Schema.String>;
1693
+ readonly createdAt: Schema.String;
1694
+ readonly updatedAt: Schema.String;
1695
+ }>>, HttpApiEndpoint.Json<typeof WorkspaceBadRequestError | typeof WorkspaceForbiddenError | typeof WorkspaceNotFoundError | typeof WorkspaceConflictError | typeof WorkspaceBadGatewayError | typeof WorkspaceServiceUnavailableError | typeof WorkspaceInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"bindWorkspace", "POST", "/:workspaceId/bind", HttpApiEndpoint.StringTree<Schema.Struct<{
1427
1696
  readonly workspaceId: Schema.String;
1428
1697
  }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
1429
1698
  readonly ownerUserId: Schema.String;
@@ -1705,6 +1974,12 @@ export declare const WorkspacesGroup: HttpApiGroup.HttpApiGroup<"workspaces", Ht
1705
1974
  readonly runningMs: Schema.Number;
1706
1975
  readonly boundMs: Schema.Number;
1707
1976
  }>>;
1977
+ /**
1978
+ * The executor booted under an owner map (`SEALANT_CAPTURE_OWNER_MAP`): its restores give each
1979
+ * mapped person's saved directory to them and the worktree to the group. Absent from a control
1980
+ * plane that predates it; false from a daemon that predates it.
1981
+ */
1982
+ readonly ownerMap: Schema.optional<Schema.Boolean>;
1708
1983
  }>>, HttpApiEndpoint.Json<typeof WorkspaceBadRequestError | typeof WorkspaceNotFoundError | typeof WorkspaceConflictError | typeof WorkspaceInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getWorkspace", "GET", "/:workspaceId", HttpApiEndpoint.StringTree<Schema.Struct<{
1709
1984
  readonly workspaceId: Schema.String;
1710
1985
  }>>, HttpApiEndpoint.StringTree<Schema.Struct<{
@@ -1956,6 +2231,12 @@ export declare const WorkspacesGroup: HttpApiGroup.HttpApiGroup<"workspaces", Ht
1956
2231
  readonly runningMs: Schema.Number;
1957
2232
  readonly boundMs: Schema.Number;
1958
2233
  }>>;
2234
+ /**
2235
+ * The executor booted under an owner map (`SEALANT_CAPTURE_OWNER_MAP`): its restores give each
2236
+ * mapped person's saved directory to them and the worktree to the group. Absent from a control
2237
+ * plane that predates it; false from a daemon that predates it.
2238
+ */
2239
+ readonly ownerMap: Schema.optional<Schema.Boolean>;
1959
2240
  }>>, HttpApiEndpoint.Json<typeof WorkspaceBadRequestError | typeof WorkspaceNotFoundError | typeof WorkspaceConflictError | typeof WorkspaceInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"getWorkspaceCreate", "GET", "/idempotency-keys/:idempotencyKey", HttpApiEndpoint.StringTree<Schema.Struct<{
1960
2241
  readonly idempotencyKey: Schema.String;
1961
2242
  }>>, HttpApiEndpoint.StringTree<Schema.Struct<{
@@ -2121,6 +2402,18 @@ export declare const WorkspacesGroup: HttpApiGroup.HttpApiGroup<"workspaces", Ht
2121
2402
  /** The account's name under its provider, as it is now. */
2122
2403
  readonly name: Schema.String;
2123
2404
  }>>;
2405
+ /** The Codex account pi's ChatGPT login is made from. */
2406
+ readonly pi: Schema.optional<Schema.Struct<{
2407
+ readonly connectedAccountId: Schema.String;
2408
+ /** The account's name under its provider, as it is now. */
2409
+ readonly name: Schema.String;
2410
+ }>>;
2411
+ /** The Codex account opencode's ChatGPT login is made from. */
2412
+ readonly opencode: Schema.optional<Schema.Struct<{
2413
+ readonly connectedAccountId: Schema.String;
2414
+ /** The account's name under its provider, as it is now. */
2415
+ readonly name: Schema.String;
2416
+ }>>;
2124
2417
  }>;
2125
2418
  }>>;
2126
2419
  }>>, HttpApiEndpoint.Json<typeof WorkspaceForbiddenError | typeof WorkspaceNotFoundError | typeof WorkspaceConflictError | typeof WorkspaceInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"listWorkspaceEvents", "GET", "/:workspaceId/events", HttpApiEndpoint.StringTree<Schema.Struct<{
@@ -2229,6 +2522,25 @@ export declare const WorkspacesGroup: HttpApiGroup.HttpApiGroup<"workspaces", Ht
2229
2522
  readonly claude: Schema.optional<Schema.NullOr<Schema.String>>;
2230
2523
  readonly codex: Schema.optional<Schema.NullOr<Schema.String>>;
2231
2524
  readonly github: Schema.optional<Schema.NullOr<Schema.String>>;
2525
+ /**
2526
+ * pi's ChatGPT login, made from a Codex account of `onBehalfOfUserId` (an id or a Codex account
2527
+ * name), as the `openai-codex` entry of `<home>/.pi/agent/auth.json`. The file is merged: its
2528
+ * other entries stay, and an `openai-codex` login the person made in pi is never replaced.
2529
+ */
2530
+ readonly pi: Schema.optional<Schema.NullOr<Schema.String>>;
2531
+ /**
2532
+ * opencode's ChatGPT login, made the same way, as the `openai` entry of opencode's `auth.json`
2533
+ * (`<home>/.local/share/opencode/auth.json`, followed through links to where it really is, which
2534
+ * must be inside the home). A login the person made in opencode is never replaced or removed.
2535
+ */
2536
+ readonly opencode: Schema.optional<Schema.NullOr<Schema.String>>;
2537
+ /**
2538
+ * Write what the person has connected and leave out the rest: a provider whose account is refused
2539
+ * (`connected-account-missing`, `-invalid` or `-unsupported`) is not written, its login is removed
2540
+ * from the home as `null` would remove it, and the answer's `skipped` says which and why, instead
2541
+ * of the whole put failing. Every other refusal still fails the put. Default false.
2542
+ */
2543
+ readonly partial: Schema.optional<Schema.Boolean>;
2232
2544
  }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
2233
2545
  readonly workspaceId: Schema.String;
2234
2546
  /** The launch attempt whose executor was written. */
@@ -2255,8 +2567,31 @@ export declare const WorkspacesGroup: HttpApiGroup.HttpApiGroup<"workspaces", Ht
2255
2567
  /** The account's name under its provider, as it is now. */
2256
2568
  readonly name: Schema.String;
2257
2569
  }>>;
2570
+ /** The Codex account pi's ChatGPT login is made from. */
2571
+ readonly pi: Schema.optional<Schema.Struct<{
2572
+ readonly connectedAccountId: Schema.String;
2573
+ /** The account's name under its provider, as it is now. */
2574
+ readonly name: Schema.String;
2575
+ }>>;
2576
+ /** The Codex account opencode's ChatGPT login is made from. */
2577
+ readonly opencode: Schema.optional<Schema.Struct<{
2578
+ readonly connectedAccountId: Schema.String;
2579
+ /** The account's name under its provider, as it is now. */
2580
+ readonly name: Schema.String;
2581
+ }>>;
2258
2582
  }>;
2259
2583
  }>;
2584
+ /** A partial put's left-out providers, in the order named (empty when all were written). */
2585
+ readonly skipped: Schema.optional<Schema.$Array<Schema.Struct<{
2586
+ readonly provider: Schema.Literals<readonly ["claude", "codex", "github", "pi", "opencode"]>;
2587
+ /**
2588
+ * The account's refusal, or `login-file-unusable`: pi's or opencode's `auth.json` in the home
2589
+ * cannot be written there (not a regular file, or really outside the home).
2590
+ */
2591
+ readonly code: Schema.Literals<readonly ["connected-account-missing", "connected-account-invalid", "connected-account-unsupported", "login-file-unusable"]>;
2592
+ /** The refusal's words, as a whole put would have answered them. */
2593
+ readonly message: Schema.String;
2594
+ }>>>;
2260
2595
  }>>, HttpApiEndpoint.Json<typeof WorkspaceBadRequestError | typeof WorkspaceForbiddenError | typeof WorkspaceNotFoundError | typeof WorkspaceConflictError | typeof WorkspaceBadGatewayError | typeof WorkspaceServiceUnavailableError | typeof WorkspaceInternalServerError>, never, never> | HttpApiEndpoint.HttpApiEndpoint<"recoverWorkspace", "POST", "/:workspaceId/recover", HttpApiEndpoint.StringTree<Schema.Struct<{
2261
2596
  readonly workspaceId: Schema.String;
2262
2597
  }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
@@ -2290,6 +2625,21 @@ export declare const WorkspacesGroup: HttpApiGroup.HttpApiGroup<"workspaces", Ht
2290
2625
  readonly workspaceId: Schema.String;
2291
2626
  }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
2292
2627
  readonly ownerUserId: Schema.String;
2628
+ /**
2629
+ * The owner map the claim needs (a capture source's `ownerMap`), or `null` for none. The daemon
2630
+ * reads its map only at boot, so a re-plan restores under the map the executor was launched
2631
+ * with: when this is given and differs from that map (including a standby launched with none),
2632
+ * the re-plan is refused with `409` code `owner-map-mismatch` before the daemon is reached.
2633
+ * Omitted, nothing is compared.
2634
+ */
2635
+ readonly expectedOwnerMap: Schema.optional<Schema.NullOr<Schema.Struct<{
2636
+ readonly gid: Schema.Number;
2637
+ readonly worktreeUid: Schema.Number;
2638
+ readonly people: Schema.$Array<Schema.Struct<{
2639
+ readonly id: Schema.String;
2640
+ readonly uid: Schema.Number;
2641
+ }>>;
2642
+ }>>>;
2293
2643
  }>>, HttpApiEndpoint.StringTree<never>, HttpApiEndpoint.Json<Schema.Struct<{
2294
2644
  readonly worktreeId: Schema.String;
2295
2645
  readonly epoch: Schema.Number;
@@ -1,6 +1,7 @@
1
1
  import { Schema } from "effect";
2
2
  import { HttpApiEndpoint, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi";
3
3
  import { BudgetExceededError } from "./budgets.js";
4
+ import { connectedAccountProviderSchema } from "./connected-accounts.js";
4
5
  import { runCommandSchema, runSchema } from "./runs.js";
5
6
  import { workspaceProcessUserSchema } from "./sessions.js";
6
7
  const NonEmptyString = Schema.String.check(Schema.isNonEmpty(), Schema.isTrimmed());
@@ -169,6 +170,82 @@ export const execWorkspaceRequestSchema = Schema.Struct({
169
170
  /** Run every command as this Linux user (see `workspaceProcessUserSchema`). */
170
171
  user: Schema.optional(workspaceProcessUserSchema),
171
172
  });
173
+ /** The `harnessId` stamped on runs that apply a person's dotfiles (`applyWorkspaceDotfiles`). */
174
+ export const dotfilesRunHarnessId = "dotfiles";
175
+ /** How a dotfiles tree is applied: chezmoi and stow layouts are detected by `auto`. */
176
+ export const workspaceDotfilesManagerSchema = Schema.Literals(["auto", "chezmoi", "stow", "copy"]);
177
+ /**
178
+ * A dotfiles repository cloned as the person (no credential reaches the workspace: a repository
179
+ * only the person's own identity can reach is resolved by the caller and sent as an archive).
180
+ */
181
+ export const applyWorkspaceDotfilesRepositorySchema = Schema.Struct({
182
+ /** An `https://` clone URL with no credential in it (no `user:token@`). */
183
+ url: NonEmptyString.check(Schema.isPattern(/^https:\/\/[^\s'"\\]+$/)),
184
+ /** Branch or tag; omitted clones the remote's default branch. */
185
+ ref: Schema.optional(NonEmptyString),
186
+ manager: Schema.optional(workspaceDotfilesManagerSchema),
187
+ /** Run the bootstrap command after applying (when the checkout has it). Default true. */
188
+ bootstrap: Schema.optional(Schema.Boolean),
189
+ /** Relative to the checkout; default `./install.sh`. */
190
+ bootstrapCommand: Schema.optional(NonEmptyString),
191
+ });
192
+ /** A caller-resolved dotfiles tree, as at create (`spec.runtime.dotfilesArchives`). */
193
+ export const applyWorkspaceDotfilesArchiveSchema = Schema.Struct({
194
+ /** base64 of a `.tar.gz`; at most 6 MiB of base64 (about 4 MiB decoded). */
195
+ data: Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(6 * 1024 * 1024), Schema.isPattern(/^[A-Za-z0-9+/]+={0,2}$/)),
196
+ manager: Schema.optional(workspaceDotfilesManagerSchema),
197
+ /** Where the tree lands: `home` (default) or `config` (`~/.config`, copy manager only). */
198
+ target: Schema.optional(Schema.Literals(["home", "config"])),
199
+ /** Run `./install.sh` (or `bootstrapCommand`) after applying when present. Default true. */
200
+ bootstrap: Schema.optional(Schema.Boolean),
201
+ bootstrapCommand: Schema.optional(NonEmptyString),
202
+ });
203
+ /**
204
+ * Apply a person's dotfiles into their home of a RUNNING workspace, as their Linux user
205
+ * (docs/connected-accounts-design.md §6g; Mend's ADR 0016 decision 11): the same sources as a
206
+ * create's dotfiles (a repository, archives, or both; the repository first, archives after in
207
+ * order) and the same applier (sealantd's: chezmoi, stow or copy, then each tree's bootstrap).
208
+ *
209
+ * `user` must exist and must not be root or in root's group, and `home` must be its passwd home: an
210
+ * existing directory of the user's, reached without a symbolic link. The clone, chezmoi, stow and the
211
+ * bootstrap run as the user; sealantd's archive staging and `copy` manager still run as root inside
212
+ * the home and follow links the person planted there, until sealantd's fix for it lands (see
213
+ * docs/connected-accounts-design.md §6g). `onBehalfOfUserId` names whose dotfiles they are: it is
214
+ * recorded on the run, and a home whose logins another person holds is refused (`home-held`). Answered `202` with a run (`harnessId`
215
+ * `dotfiles`) once the archives are staged: the run's first `processStarted` is the bootstrap,
216
+ * started once every file is applied; its output and exit are the run's. The run completes with the
217
+ * bootstrap's exit code (0 when no tree had one) and fails, with the daemon's words, when the apply
218
+ * was refused or the bootstrap ran past 30 minutes (it is stopped).
219
+ */
220
+ export const applyWorkspaceDotfilesRequestSchema = Schema.Struct({
221
+ /** The workspace's owner (owner scope; uniform 404 otherwise). */
222
+ ownerUserId: NonEmptyString,
223
+ /** The Sealant user whose dotfiles these are: recorded on the run (`metadata.dotfiles`). */
224
+ onBehalfOfUserId: NonEmptyString,
225
+ /** The Linux user to apply as (a login name or a uid); never root. */
226
+ user: workspaceProcessUserSchema,
227
+ /** The user's passwd home: absolute, normalised, never under `/workspace`. */
228
+ home: NonEmptyString,
229
+ repository: Schema.optional(applyWorkspaceDotfilesRepositorySchema),
230
+ archives: Schema.optional(Schema.Array(applyWorkspaceDotfilesArchiveSchema).check(Schema.isMaxLength(4))),
231
+ });
232
+ /**
233
+ * Stable `code`s of the `WorkspaceConflictError` a dotfiles apply answers, nothing applied:
234
+ * `workspace-not-running` (no ready executor), `dotfiles-user-unsupported` (the workspace's
235
+ * sealantd cannot apply dotfiles as a user), `user-unknown` (no such user: make it first),
236
+ * `user-root` (the user is root or in root's group), `home-mismatch` (the home is not the user's
237
+ * passwd home), `home-unusable` (the home is missing, not a directory, not the user's, or reached
238
+ * through a symbolic link), `home-held` (another person's logins are held in the home).
239
+ */
240
+ export const workspaceDotfilesConflictCodes = [
241
+ "workspace-not-running",
242
+ "dotfiles-user-unsupported",
243
+ "user-unknown",
244
+ "user-root",
245
+ "home-mismatch",
246
+ "home-unusable",
247
+ "home-held",
248
+ ];
172
249
  /**
173
250
  * Bind a standby workspace's working directory, or a bindable extra mount, to one subdirectory of
174
251
  * its root (sealantd ADR-0014). `mountPath` defaults to the working directory; an empty `subpath`
@@ -357,6 +434,12 @@ export const workspaceCaptureStatusSchema = Schema.Struct({
357
434
  * past its bound, and from a daemon or control plane that predates it.
358
435
  */
359
436
  overdue: Schema.optional(captureOverdueSchema),
437
+ /**
438
+ * The executor booted under an owner map (`SEALANT_CAPTURE_OWNER_MAP`): its restores give each
439
+ * mapped person's saved directory to them and the worktree to the group. Absent from a control
440
+ * plane that predates it; false from a daemon that predates it.
441
+ */
442
+ ownerMap: Schema.optional(Schema.Boolean),
360
443
  });
361
444
  /**
362
445
  * Re-plan a capture-sourced workspace (sealantd 0.15 `capture.replan`, the claim hook): the daemon
@@ -367,6 +450,18 @@ export const workspaceCaptureStatusSchema = Schema.Struct({
367
450
  */
368
451
  export const replanWorkspaceCaptureRequestSchema = Schema.Struct({
369
452
  ownerUserId: NonEmptyString,
453
+ /**
454
+ * The owner map the claim needs (a capture source's `ownerMap`), or `null` for none. The daemon
455
+ * reads its map only at boot, so a re-plan restores under the map the executor was launched
456
+ * with: when this is given and differs from that map (including a standby launched with none),
457
+ * the re-plan is refused with `409` code `owner-map-mismatch` before the daemon is reached.
458
+ * Omitted, nothing is compared.
459
+ */
460
+ expectedOwnerMap: Schema.optional(Schema.NullOr(Schema.Struct({
461
+ gid: Schema.Number,
462
+ worktreeUid: Schema.Number,
463
+ people: Schema.Array(Schema.Struct({ id: Schema.String, uid: Schema.Number })),
464
+ }))),
370
465
  });
371
466
  export const workspaceCaptureReplannedSchema = Schema.Struct({
372
467
  worktreeId: NonEmptyString,
@@ -499,8 +594,34 @@ export const inspectWorkspaceImageResponseSchema = Schema.Struct({
499
594
  /** The image's per-person capability (`unknown` when none has been built yet). */
500
595
  personLayout: workspaceImagePersonLayoutSchema,
501
596
  });
502
- /** The providers a home holds logins for (docs/connected-accounts-design.md §6c). */
503
- export const workspaceCredentialHomeProviders = ["claude", "codex", "github"];
597
+ /**
598
+ * Stable codes of a refused connected account, carried with the account's `provider` by the error
599
+ * that refuses it (a create's or a credentials put's): `connected-account-missing`
600
+ * (`WorkspaceNotFoundError`, 404: the person has no such account, or it is archived or someone
601
+ * else's) and `connected-account-invalid` (`WorkspaceConflictError`, 409: the account is marked
602
+ * invalid, or holds an unusable credential; reconnect it) and `connected-account-unsupported`
603
+ * (`WorkspaceConflictError`, 409: the account cannot serve what was asked, as a Codex API key cannot
604
+ * be pi's or opencode's ChatGPT login). Nothing was written either way.
605
+ */
606
+ export const connectedAccountRefusalCodes = [
607
+ "connected-account-missing",
608
+ "connected-account-invalid",
609
+ "connected-account-unsupported",
610
+ ];
611
+ /**
612
+ * The providers a home holds logins for (docs/connected-accounts-design.md §6c). `pi` and
613
+ * `opencode` are the person's ChatGPT login (a Codex connected account), written into each tool's
614
+ * own `auth.json`.
615
+ */
616
+ export const workspaceCredentialHomeProviders = [
617
+ "claude",
618
+ "codex",
619
+ "github",
620
+ "pi",
621
+ "opencode",
622
+ ];
623
+ /** The connected account's provider each home provider's login is made from. */
624
+ export const workspaceCredentialHomeAccountProvider = { claude: "claude", codex: "codex", github: "github", pi: "codex", opencode: "codex" };
504
625
  /**
505
626
  * Put one person's logins into one home of a RUNNING workspace (docs/connected-accounts-design.md
506
627
  * §6c). A home holds one person's logins while it is held: the first put names its person, a put
@@ -532,6 +653,25 @@ export const putWorkspaceCredentialsRequestSchema = Schema.Struct({
532
653
  claude: Schema.optional(Schema.NullOr(NonEmptyString)),
533
654
  codex: Schema.optional(Schema.NullOr(NonEmptyString)),
534
655
  github: Schema.optional(Schema.NullOr(NonEmptyString)),
656
+ /**
657
+ * pi's ChatGPT login, made from a Codex account of `onBehalfOfUserId` (an id or a Codex account
658
+ * name), as the `openai-codex` entry of `<home>/.pi/agent/auth.json`. The file is merged: its
659
+ * other entries stay, and an `openai-codex` login the person made in pi is never replaced.
660
+ */
661
+ pi: Schema.optional(Schema.NullOr(NonEmptyString)),
662
+ /**
663
+ * opencode's ChatGPT login, made the same way, as the `openai` entry of opencode's `auth.json`
664
+ * (`<home>/.local/share/opencode/auth.json`, followed through links to where it really is, which
665
+ * must be inside the home). A login the person made in opencode is never replaced or removed.
666
+ */
667
+ opencode: Schema.optional(Schema.NullOr(NonEmptyString)),
668
+ /**
669
+ * Write what the person has connected and leave out the rest: a provider whose account is refused
670
+ * (`connected-account-missing`, `-invalid` or `-unsupported`) is not written, its login is removed
671
+ * from the home as `null` would remove it, and the answer's `skipped` says which and why, instead
672
+ * of the whole put failing. Every other refusal still fails the put. Default false.
673
+ */
674
+ partial: Schema.optional(Schema.Boolean),
535
675
  });
536
676
  /** One account whose copy a home holds. */
537
677
  export const workspaceHomeAccountSchema = Schema.Struct({
@@ -549,14 +689,31 @@ export const workspaceCredentialHomeSchema = Schema.Struct({
549
689
  claude: Schema.optional(workspaceHomeAccountSchema),
550
690
  codex: Schema.optional(workspaceHomeAccountSchema),
551
691
  github: Schema.optional(workspaceHomeAccountSchema),
692
+ /** The Codex account pi's ChatGPT login is made from. */
693
+ pi: Schema.optional(workspaceHomeAccountSchema),
694
+ /** The Codex account opencode's ChatGPT login is made from. */
695
+ opencode: Schema.optional(workspaceHomeAccountSchema),
552
696
  }),
553
697
  });
698
+ /** A provider a partial put left out, and why (its account's refusal). */
699
+ export const workspaceCredentialSkipSchema = Schema.Struct({
700
+ provider: Schema.Literals(workspaceCredentialHomeProviders),
701
+ /**
702
+ * The account's refusal, or `login-file-unusable`: pi's or opencode's `auth.json` in the home
703
+ * cannot be written there (not a regular file, or really outside the home).
704
+ */
705
+ code: Schema.Literals([...connectedAccountRefusalCodes, "login-file-unusable"]),
706
+ /** The refusal's words, as a whole put would have answered them. */
707
+ message: Schema.String,
708
+ });
554
709
  export const putWorkspaceCredentialsResponseSchema = Schema.Struct({
555
710
  workspaceId: NonEmptyString,
556
711
  /** The launch attempt whose executor was written. */
557
712
  runId: NonEmptyString,
558
713
  /** The home as it is after the put. */
559
714
  home: workspaceCredentialHomeSchema,
715
+ /** A partial put's left-out providers, in the order named (empty when all were written). */
716
+ skipped: Schema.optional(Schema.Array(workspaceCredentialSkipSchema)),
560
717
  });
561
718
  /** Release one home: its login files are removed and its record deleted. */
562
719
  export const releaseWorkspaceCredentialsQuerySchema = Schema.Struct({
@@ -582,7 +739,9 @@ export const listWorkspaceCredentialsResponseSchema = Schema.Struct({
582
739
  /**
583
740
  * Stable `code`s of the `WorkspaceConflictError` the credential routes answer:
584
741
  * `workspace-not-running` (no ready executor), `connected-account-invalid` (a named account is
585
- * marked invalid: reconnect it), `home-held` (the home holds another person's logins: release it
742
+ * marked invalid or unusable: reconnect it; `provider` names its provider),
743
+ * `connected-account-unsupported` (a Codex account named for pi or opencode is not a ChatGPT
744
+ * login), `home-held` (the home holds another person's logins: release it
586
745
  * first; `/root` while the launch's own logins are at `$HOME`, or for anyone but the workspace's
587
746
  * owner), `home-unusable` (the home does not exist, is not a directory, is reached through a
588
747
  * symbolic link, or a login directory in it links outside it), `home-busy` (another write into the
@@ -591,6 +750,7 @@ export const listWorkspaceCredentialsResponseSchema = Schema.Struct({
591
750
  export const workspaceCredentialsConflictCodes = [
592
751
  "workspace-not-running",
593
752
  "connected-account-invalid",
753
+ "connected-account-unsupported",
594
754
  "home-held",
595
755
  "home-unusable",
596
756
  "home-busy",
@@ -889,12 +1049,18 @@ export class WorkspaceForbiddenError extends Schema.TaggedErrorClass()("Workspac
889
1049
  }
890
1050
  export class WorkspaceNotFoundError extends Schema.TaggedErrorClass()("WorkspaceNotFoundError", {
891
1051
  message: Schema.String,
1052
+ /** A stable reason, where one applies (`connected-account-missing`). */
1053
+ code: Schema.optional(NonEmptyString),
1054
+ /** The provider of the connected account the reason is about. */
1055
+ provider: Schema.optional(connectedAccountProviderSchema),
892
1056
  }, { httpApiStatus: 404 }) {
893
1057
  }
894
1058
  export class WorkspaceConflictError extends Schema.TaggedErrorClass()("WorkspaceConflictError", {
895
1059
  message: Schema.String,
896
1060
  /** A stable reason, where one applies (`create-cancelled`: the create's key was cancelled). */
897
1061
  code: Schema.optional(NonEmptyString),
1062
+ /** The provider of the connected account the reason is about (`connected-account-invalid`). */
1063
+ provider: Schema.optional(connectedAccountProviderSchema),
898
1064
  }, { httpApiStatus: 409 }) {
899
1065
  }
900
1066
  export class WorkspaceBadGatewayError extends Schema.TaggedErrorClass()("WorkspaceBadGatewayError", {
@@ -1023,6 +1189,27 @@ HttpApiEndpoint.post("execWorkspace", "/:workspaceId/exec", {
1023
1189
  ],
1024
1190
  }))
1025
1191
  .add(
1192
+ // Async like exec: 202 + the queued run once the archives are staged; poll `GET /v1/runs/:runId`.
1193
+ HttpApiEndpoint.post("applyWorkspaceDotfiles", "/:workspaceId/dotfiles", {
1194
+ params: workspaceIdParams,
1195
+ payload: applyWorkspaceDotfilesRequestSchema,
1196
+ success: runSchema.pipe(HttpApiSchema.status(202)),
1197
+ error: [
1198
+ // Root, a malformed home, a URL with a credential, or nothing to apply.
1199
+ WorkspaceBadRequestError,
1200
+ // Only a service key may apply a person's dotfiles.
1201
+ WorkspaceForbiddenError,
1202
+ WorkspaceNotFoundError,
1203
+ // See `workspaceDotfilesConflictCodes`.
1204
+ WorkspaceConflictError,
1205
+ // The executor did not confirm the staging.
1206
+ WorkspaceBadGatewayError,
1207
+ // Core cannot reach the executor.
1208
+ WorkspaceServiceUnavailableError,
1209
+ WorkspaceInternalServerError,
1210
+ ],
1211
+ }))
1212
+ .add(
1026
1213
  // Async: 202 = the stop was accepted and enqueued; the worker removes the container and the
1027
1214
  // workspace transitions to "stopped". Idempotent — stopping a stopped workspace is a no-op 202.
1028
1215
  HttpApiEndpoint.post("stopWorkspace", "/:workspaceId/stop", {
@@ -1088,7 +1275,7 @@ HttpApiEndpoint.post("putWorkspaceCredentials", "/:workspaceId/credentials", {
1088
1275
  WorkspaceBadRequestError,
1089
1276
  // The caller may not act for the people named (only a service key may).
1090
1277
  WorkspaceForbiddenError,
1091
- // The workspace or a named account (unknown, someone else's, archived).
1278
+ // The workspace, or a named account (`connected-account-missing` with its `provider`).
1092
1279
  WorkspaceNotFoundError,
1093
1280
  // See `workspaceCredentialsConflictCodes`.
1094
1281
  WorkspaceConflictError,
package/dist/index.d.ts CHANGED
@@ -16,3 +16,4 @@ export * from "./core-api/ssh-keys.js";
16
16
  export * from "./core-api/system.js";
17
17
  export * from "./core-api/users.js";
18
18
  export * from "./workspace-environment.js";
19
+ export * from "./capture-owner-map.js";
package/dist/index.js CHANGED
@@ -16,3 +16,4 @@ export * from "./core-api/ssh-keys.js";
16
16
  export * from "./core-api/system.js";
17
17
  export * from "./core-api/users.js";
18
18
  export * from "./workspace-environment.js";
19
+ export * from "./capture-owner-map.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sealant/api-contracts-next",
3
- "version": "0.39.0-next.695",
3
+ "version": "0.39.0-next.700",
4
4
  "description": "Contract-first HTTP API definitions for Sealant services",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -17,6 +17,10 @@
17
17
  "./workspace-environment": {
18
18
  "types": "./dist/workspace-environment.d.ts",
19
19
  "import": "./dist/workspace-environment.js"
20
+ },
21
+ "./capture-owner-map": {
22
+ "types": "./dist/capture-owner-map.d.ts",
23
+ "import": "./dist/capture-owner-map.js"
20
24
  }
21
25
  },
22
26
  "files": [
@@ -28,7 +32,7 @@
28
32
  "peerDependencies": {
29
33
  "effect": ">=4.0.0-beta.85 <5"
30
34
  },
31
- "gitHead": "597db621e15babf1e340030bfe19d5786b43d626",
35
+ "gitHead": "78e1763231f005b670f7291cfb86917cec639598",
32
36
  "publishConfig": {
33
37
  "access": "public"
34
38
  }