opencode-session-port 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -2
  3. package/package.json +56 -4
  4. package/src/core/contracts/crypto.ts +66 -0
  5. package/src/core/contracts/index.ts +13 -0
  6. package/src/core/contracts/json.ts +142 -0
  7. package/src/core/contracts/session.ts +12 -0
  8. package/src/core/crypto/container.ts +686 -0
  9. package/src/core/crypto/envelope.ts +232 -0
  10. package/src/core/crypto/errors.ts +52 -0
  11. package/src/core/crypto/gzip-stream.ts +388 -0
  12. package/src/core/crypto/index.ts +13 -0
  13. package/src/core/crypto/kdf.ts +101 -0
  14. package/src/core/crypto/policy.ts +202 -0
  15. package/src/core/crypto/zero.ts +33 -0
  16. package/src/core/restore/identity.ts +202 -0
  17. package/src/core/restore/index.ts +24 -0
  18. package/src/core/restore/planner.ts +1122 -0
  19. package/src/core/sessions/collector.ts +323 -0
  20. package/src/core/sessions/errors.ts +37 -0
  21. package/src/core/sessions/guards.ts +11 -0
  22. package/src/core/sessions/index.ts +29 -0
  23. package/src/core/sessions/policy.ts +80 -0
  24. package/src/core/sessions/port.ts +88 -0
  25. package/src/core/sessions/serialize.ts +15 -0
  26. package/src/core/sessions/unsettled.ts +35 -0
  27. package/src/integrations/omo/index.ts +28 -0
  28. package/src/tui/adapters/host-client.ts +254 -0
  29. package/src/tui/adapters/index.ts +72 -0
  30. package/src/tui/adapters/location-resolver.ts +281 -0
  31. package/src/tui/adapters/project-destinations.ts +271 -0
  32. package/src/tui/adapters/session-id-inventory.ts +378 -0
  33. package/src/tui/adapters/session-import.ts +189 -0
  34. package/src/tui/adapters/session-read.ts +115 -0
  35. package/src/tui/artifacts/files.ts +285 -0
  36. package/src/tui/orchestration/checkpoint.ts +160 -0
  37. package/src/tui/orchestration/index.ts +55 -0
  38. package/src/tui/orchestration/restore-auth.ts +94 -0
  39. package/src/tui/orchestration/restore-execution.ts +381 -0
  40. package/src/tui/orchestration/restore-preflight.ts +223 -0
  41. package/src/tui/orchestration/shared.ts +75 -0
  42. package/src/tui/ui/password-dialog.tsx +251 -0
  43. package/src/tui/ui/password-flow.ts +288 -0
  44. package/src/tui/ui/restore-project-selector.ts +62 -0
  45. package/src/tui/ui/restore-report-dialog.tsx +251 -0
  46. package/src/tui/ui/restore-report-flow.ts +94 -0
  47. package/src/tui/ui/sessionport-commands.tsx +65 -0
  48. package/src/tui/wiring/composition.ts +354 -0
  49. package/src/tui/wiring/experimental-profile.ts +86 -0
  50. package/src/tui/wiring/index.ts +51 -0
  51. package/src/tui/wiring/install.ts +108 -0
  52. package/src/tui/wiring/reports.ts +340 -0
  53. package/src/tui.ts +34 -0
  54. package/tsconfig.json +18 -0
  55. package/tui.ts +12 -0
@@ -0,0 +1,354 @@
1
+ /**
2
+ * SessionPort TUI command composition (Phase 2 experimental Windows wiring).
3
+ *
4
+ * `createSessionPortHandlers` composes the completed Phase 2-4 seams into the
5
+ * two injected command callbacks consumed by `installSessionPortCommands`:
6
+ *
7
+ * `/checkpoint [artifact-path]`
8
+ * 1. resolve the active project from `context.location.directory` through the
9
+ * public `location.get` resolver (exact response matching; fail closed);
10
+ * 2. `runCheckpointFlow` — exhaustive project-scoped collection (fail, never
11
+ * truncate, on any collection cap), matching password prompt, P-15 path
12
+ * resolution, seal, exclusive encrypted write;
13
+ * 3. report path/size/session-count facts or a sanitized failure.
14
+ *
15
+ * `/restore <artifact-path>`
16
+ * 1. require the artifact path argument (nothing runs without it);
17
+ * 2. probe `session.import` capability FIRST — before any artifact read,
18
+ * decrypt, or import (O-12);
19
+ * 3. enumerate existing host projects via `project.list`, let the user select
20
+ * with `selectRestoreProject`;
21
+ * 4. call `location.get` ONLY for the selected candidate and require an exact
22
+ * project-ID/directory match (E42 side effect; OQ-16 stays OPEN);
23
+ * 5. `runRestorePreflightWithPasswordAttempts` — up to three password
24
+ * attempts, full authentication, strict parse, destination snapshot, the
25
+ * required metadata-only cross-project session-ID occupancy inventory
26
+ * (reusing the already-validated selection candidates; no second
27
+ * `project.list`, no `location.get`, no message reads), and `planRestore`
28
+ * before anything else happens;
29
+ * 6. `runRestoreExecution` — sequential parent-first imports with the
30
+ * explicit target on every call, no retries, no rollback promise;
31
+ * 7. present the completed non-empty result through the designer-owned
32
+ * keyboard-navigable report dialog (aggregate/per-session facts only); an
33
+ * empty result keeps the truthful non-success notification, and a
34
+ * dialog-display failure falls back to bounded aggregate-only copy.
35
+ *
36
+ * Hard boundaries: no shell/Git, no LLM/model call, no historical tool replay,
37
+ * no direct database access, no `project.update`; passwords stay masked and are
38
+ * owned/zeroed by the orchestration helpers; only already-sealed ciphertext is
39
+ * written and only encrypted artifacts are read. Everything is injected through
40
+ * `SessionPortHandlerDependencies`, so tests use fake clients, fake crypto,
41
+ * fake artifact I/O, fake dialogs, and a fake report presenter only.
42
+ *
43
+ * Platform policy: registration is gated in `install.ts`; this module itself
44
+ * makes no platform-support claim.
45
+ */
46
+
47
+ import type { ByteSource, CheckpointCryptoFactory } from "../../core/contracts/index.ts";
48
+ import { collectCheckpointPayload } from "../../core/sessions/index.ts";
49
+ import type { RemappedIdDeriver } from "../../core/restore/index.ts";
50
+ import type { ResolveCheckpointOutputPathOptions } from "../artifacts/files.ts";
51
+ import { createLocationResolver, LocationResolutionError } from "../adapters/location-resolver.ts";
52
+ import { createProjectDestinationsAdapter } from "../adapters/project-destinations.ts";
53
+ import { createSessionIdInventory } from "../adapters/session-id-inventory.ts";
54
+ import { createSessionReadAdapter } from "../adapters/session-read.ts";
55
+ import { createSessionImportAdapter, probeSessionImportCapability } from "../adapters/session-import.ts";
56
+ import {
57
+ runCheckpointFlow,
58
+ runRestoreExecution,
59
+ runRestorePreflightWithPasswordAttempts,
60
+ type RestoreExecutionResult,
61
+ } from "../orchestration/index.ts";
62
+ import { requireCallable, isRecordValue } from "../orchestration/shared.ts";
63
+ import { selectRestoreProject, type RestoreProjectDialog } from "../ui/restore-project-selector.ts";
64
+ import type { RestoreReportPresentationModel } from "../ui/restore-report-flow.ts";
65
+ import type { SessionPortCommandHandlers } from "../ui/sessionport-commands.tsx";
66
+ import {
67
+ EXPERIMENTAL_COLLECTION_POLICY,
68
+ EXPERIMENTAL_CRYPTO_POLICY,
69
+ EXPERIMENTAL_MAX_PASSWORD_BYTES,
70
+ } from "./experimental-profile.ts";
71
+ import {
72
+ mapRestoreExecutionToReportPresentation,
73
+ reportCheckpointFailure,
74
+ reportCheckpointOutcome,
75
+ reportRestoreAuthenticationFailed,
76
+ reportRestoreCanceled,
77
+ reportRestoreCapabilityUnavailable,
78
+ reportRestoreExecution,
79
+ reportRestoreExecutionFallback,
80
+ reportRestoreFailure,
81
+ reportRestoreUsage,
82
+ type SessionPortNotification,
83
+ } from "./reports.ts";
84
+
85
+ /* ------------------------------------------------------------------------- */
86
+ /* Injected dependency surface */
87
+ /* ------------------------------------------------------------------------- */
88
+
89
+ /** One notification sink; the install layer maps it to `context.ui.toast`. */
90
+ export type SessionPortNotifier = (notification: SessionPortNotification) => void;
91
+
92
+ /**
93
+ * Masked password provider: one call per prompt, resolves with the UTF-8 bytes
94
+ * or `undefined` on cancel. The experimental 4-KiB byte limit is always passed.
95
+ */
96
+ export type SessionPortPasswordProvider = (
97
+ mode: "checkpoint" | "restore",
98
+ maxPasswordBytes: number,
99
+ ) => Promise<Uint8Array | undefined>;
100
+
101
+ /** Explicit, fully injected command dependencies. Every effect is supplied by the caller. */
102
+ export interface SessionPortHandlerDependencies {
103
+ /** Host-provided public client (untrusted); all host methods go through the existing adapters. */
104
+ readonly client: unknown;
105
+ /** Live read of the active project directory (`context.location?.directory`). */
106
+ readonly getActiveDirectory: () => string | undefined;
107
+ /** Host dialog selection surface for the restore destination. */
108
+ readonly dialog: RestoreProjectDialog;
109
+ /** Sanitized notification sink. */
110
+ readonly notify: SessionPortNotifier;
111
+ /** Presents the completed restore report (designer-owned dialog in production). */
112
+ readonly showRestoreReport: (model: RestoreReportPresentationModel) => void;
113
+ /** Masked password provider (real UI or a fake in tests). */
114
+ readonly requestPassword: SessionPortPasswordProvider;
115
+ /** Initialized crypto factory; production uses the experimental Windows profile. */
116
+ readonly cryptoFactory: CheckpointCryptoFactory;
117
+ /** Remapped-ID deriver for restore planning. */
118
+ readonly idDeriver: RemappedIdDeriver;
119
+ /** P-15 artifact output-path resolver. */
120
+ readonly resolveCheckpointOutputPath: (options: ResolveCheckpointOutputPathOptions) => Promise<string>;
121
+ /** Exclusive encrypted-artifact writer (ciphertext only). */
122
+ readonly writeEncryptedArtifact: (targetPath: string, source: ByteSource) => Promise<number>;
123
+ /** Lazy encrypted-artifact reader (no IO until iterated). */
124
+ readonly readEncryptedArtifact: (targetPath: string) => ByteSource;
125
+ }
126
+
127
+ /* ------------------------------------------------------------------------- */
128
+ /* createSessionPortHandlers */
129
+ /* ------------------------------------------------------------------------- */
130
+
131
+ /**
132
+ * Builds the two command callbacks. Dependency shape is validated fail-closed
133
+ * before any handler runs; host/API and orchestration errors are converted to
134
+ * sanitized reports inside each handler and are never rethrown to the host.
135
+ */
136
+ export function createSessionPortHandlers(dependencies: SessionPortHandlerDependencies): SessionPortCommandHandlers {
137
+ if (!isRecordValue(dependencies)) {
138
+ throw new TypeError("createSessionPortHandlers requires an explicit dependency object");
139
+ }
140
+ requireCallable(dependencies.getActiveDirectory, "getActiveDirectory");
141
+ requireCallable(dependencies.notify, "notify");
142
+ requireCallable(dependencies.showRestoreReport, "showRestoreReport");
143
+ requireCallable(dependencies.requestPassword, "requestPassword");
144
+ requireCallable(dependencies.resolveCheckpointOutputPath, "resolveCheckpointOutputPath");
145
+ requireCallable(dependencies.writeEncryptedArtifact, "writeEncryptedArtifact");
146
+ requireCallable(dependencies.readEncryptedArtifact, "readEncryptedArtifact");
147
+ if (!isRecordValue(dependencies.cryptoFactory) || typeof dependencies.cryptoFactory.create !== "function") {
148
+ throw new TypeError("createSessionPortHandlers requires a CheckpointCryptoFactory with a create(policy) function");
149
+ }
150
+ if (!isRecordValue(dependencies.idDeriver) || typeof dependencies.idDeriver.derive !== "function") {
151
+ throw new TypeError("createSessionPortHandlers requires a RemappedIdDeriver with a derive(namespace, identity, id) function");
152
+ }
153
+ if (!isRecordValue(dependencies.dialog) || typeof dependencies.dialog.select !== "function") {
154
+ throw new TypeError("createSessionPortHandlers requires a dialog with a select(options) function");
155
+ }
156
+
157
+ return {
158
+ async checkpoint(rawInput: string | undefined): Promise<void> {
159
+ await runCheckpointCommand(dependencies, rawInput);
160
+ },
161
+ async restore(rawInput: string | undefined): Promise<void> {
162
+ await runRestoreCommand(dependencies, rawInput);
163
+ },
164
+ };
165
+ }
166
+
167
+ /* ------------------------------------------------------------------------- */
168
+ /* /checkpoint */
169
+ /* ------------------------------------------------------------------------- */
170
+
171
+ async function runCheckpointCommand(
172
+ dependencies: SessionPortHandlerDependencies,
173
+ rawInput: string | undefined,
174
+ ): Promise<void> {
175
+ const explicitOutputPath = parseCheckpointOutputArgument(rawInput);
176
+ try {
177
+ const activeDirectory = dependencies.getActiveDirectory();
178
+ if (typeof activeDirectory !== "string" || activeDirectory.trim().length === 0) {
179
+ throw new LocationResolutionError(
180
+ "invalid-request",
181
+ "The active project directory is unavailable; checkpoint requires an active project",
182
+ );
183
+ }
184
+ // One explicit location.get for the active directory (E42 side effect).
185
+ const resolved = await createLocationResolver(dependencies.client).resolveActiveProject(activeDirectory);
186
+ const readPort = createSessionReadAdapter(dependencies.client);
187
+ const outcome = await runCheckpointFlow(readPort, {
188
+ projectID: resolved.projectID,
189
+ projectDirectory: resolved.directory,
190
+ policy: EXPERIMENTAL_COLLECTION_POLICY,
191
+ cryptoFactory: dependencies.cryptoFactory,
192
+ cryptoPolicy: EXPERIMENTAL_CRYPTO_POLICY,
193
+ resolveCheckpointOutputPath: dependencies.resolveCheckpointOutputPath,
194
+ writeEncryptedArtifact: dependencies.writeEncryptedArtifact,
195
+ requestPassword: (purpose) => dependencies.requestPassword(purpose, EXPERIMENTAL_MAX_PASSWORD_BYTES),
196
+ ...(explicitOutputPath !== undefined ? { explicitOutputPath } : {}),
197
+ });
198
+ dependencies.notify(reportCheckpointOutcome(outcome));
199
+ } catch (error) {
200
+ dependencies.notify(reportCheckpointFailure(error));
201
+ }
202
+ }
203
+
204
+ /* ------------------------------------------------------------------------- */
205
+ /* /restore */
206
+ /* ------------------------------------------------------------------------- */
207
+
208
+ async function runRestoreCommand(
209
+ dependencies: SessionPortHandlerDependencies,
210
+ rawInput: string | undefined,
211
+ ): Promise<void> {
212
+ const artifactPath = parseRestoreArtifactArgument(rawInput);
213
+ if (artifactPath === undefined) {
214
+ dependencies.notify(reportRestoreUsage());
215
+ return;
216
+ }
217
+
218
+ // O-12: capability absence is the first check — before any file read,
219
+ // decrypt, destination enumeration, or import.
220
+ const capability = probeSessionImportCapability(dependencies.client);
221
+ if (!capability.available) {
222
+ dependencies.notify(reportRestoreCapabilityUnavailable());
223
+ return;
224
+ }
225
+
226
+ let executionStarted = false;
227
+ try {
228
+ // Candidate enumeration and user selection happen before any location.get
229
+ // or artifact access; a canceled selection changes nothing.
230
+ const destinations = await createProjectDestinationsAdapter(dependencies.client).listDestinations();
231
+ const selected = await selectRestoreProject(dependencies.dialog, destinations);
232
+ if (selected === undefined) {
233
+ dependencies.notify(reportRestoreCanceled());
234
+ return;
235
+ }
236
+
237
+ // location.get ONLY for the selected candidate; exact ID/directory match
238
+ // is required before any artifact read/decrypt/import (E42; OQ-16 OPEN).
239
+ const resolved = await createLocationResolver(dependencies.client).resolveSelectedProject({
240
+ projectID: selected.projectID,
241
+ targetDirectory: selected.targetDirectory,
242
+ });
243
+
244
+ // Destination snapshot uses the same explicit project scope and the
245
+ // experimental collection bounds; it is read by runRestorePreflight only
246
+ // after full authentication and strict parsing. The cross-project
247
+ // session-ID inventory reuses the already-validated selection candidates
248
+ // (no second project.list) and scans only session metadata (no message
249
+ // reads, no location.get, no import).
250
+ const readPort = createSessionReadAdapter(dependencies.client);
251
+ const sessionIdInventory = createSessionIdInventory({
252
+ readPort,
253
+ projects: destinations,
254
+ selectedProjectID: resolved.projectID,
255
+ policy: EXPERIMENTAL_COLLECTION_POLICY,
256
+ });
257
+ const outcome = await runRestorePreflightWithPasswordAttempts(dependencies.client, {
258
+ artifactPath,
259
+ destinationProjectID: resolved.projectID,
260
+ targetDirectory: resolved.directory,
261
+ cryptoFactory: dependencies.cryptoFactory,
262
+ cryptoPolicy: EXPERIMENTAL_CRYPTO_POLICY,
263
+ readEncryptedArtifact: dependencies.readEncryptedArtifact,
264
+ requestPassword: (purpose) => dependencies.requestPassword(purpose, EXPERIMENTAL_MAX_PASSWORD_BYTES),
265
+ readDestination: async (projectID) =>
266
+ (await collectCheckpointPayload(readPort, { projectID, policy: EXPERIMENTAL_COLLECTION_POLICY })).sessions,
267
+ readGloballyVisibleSessionIDs: () => sessionIdInventory.readGloballyVisibleSessionIDs(),
268
+ idDeriver: dependencies.idDeriver,
269
+ });
270
+
271
+ if (outcome.status === "capability-unavailable") {
272
+ dependencies.notify(reportRestoreCapabilityUnavailable());
273
+ return;
274
+ }
275
+ if (outcome.status === "canceled") {
276
+ dependencies.notify(reportRestoreCanceled());
277
+ return;
278
+ }
279
+ if (outcome.status === "authentication-failed") {
280
+ dependencies.notify(reportRestoreAuthenticationFailed());
281
+ return;
282
+ }
283
+
284
+ // Only a completed preflight reaches execution; every import carries the
285
+ // explicit target directory through the existing adapter (O-16).
286
+ const importer = createSessionImportAdapter(dependencies.client);
287
+ executionStarted = true;
288
+ const result = await runRestoreExecution({
289
+ plan: outcome.plan,
290
+ importer,
291
+ targetDirectory: outcome.targetDirectory,
292
+ });
293
+ reportRestoreOutcome(dependencies, result);
294
+ } catch (error) {
295
+ dependencies.notify(reportRestoreFailure(error, executionStarted));
296
+ }
297
+ }
298
+
299
+ /* ------------------------------------------------------------------------- */
300
+ /* Restore outcome presentation */
301
+ /* ------------------------------------------------------------------------- */
302
+
303
+ /**
304
+ * Presents one completed restore execution. A non-empty result is mapped to the
305
+ * designer's `RestoreReportPresentationModel` and shown in the keyboard-
306
+ * navigable report dialog (up to the experimental session cap of outcomes, one
307
+ * navigable entry each) instead of a per-session toast. An empty result keeps
308
+ * the truthful non-success notification.
309
+ *
310
+ * If the report dialog (or the mapping into its model) throws after execution,
311
+ * the execution is still reported as finished through bounded aggregate-only
312
+ * copy: a display failure is never reported as an execution failure, and no raw
313
+ * exception text or per-session content reaches the notification.
314
+ */
315
+ function reportRestoreOutcome(dependencies: SessionPortHandlerDependencies, result: RestoreExecutionResult): void {
316
+ if (result.summary.total === 0) {
317
+ dependencies.notify(reportRestoreExecution(result));
318
+ return;
319
+ }
320
+ try {
321
+ dependencies.showRestoreReport(mapRestoreExecutionToReportPresentation(result));
322
+ } catch {
323
+ dependencies.notify(reportRestoreExecutionFallback(result));
324
+ }
325
+ }
326
+
327
+ /* ------------------------------------------------------------------------- */
328
+ /* Raw-argument parsing (no flag syntax; no trimming of path values) */
329
+ /* ------------------------------------------------------------------------- */
330
+
331
+ /**
332
+ * `/checkpoint` accepts one optional raw argument: the explicit artifact output
333
+ * path. `undefined` or an all-whitespace argument uses the P-15 default
334
+ * `.sessionport/` location; any other argument is trimmed of surrounding
335
+ * whitespace and passed to the P-15 resolver verbatim (no flag parsing, no
336
+ * shell expansion, no quote handling).
337
+ */
338
+ export function parseCheckpointOutputArgument(rawInput: string | undefined): string | undefined {
339
+ if (rawInput === undefined) return undefined;
340
+ const trimmed = rawInput.trim();
341
+ return trimmed.length === 0 ? undefined : trimmed;
342
+ }
343
+
344
+ /**
345
+ * `/restore` requires one raw argument: the artifact path. `undefined` or an
346
+ * all-whitespace argument yields `undefined` so the caller reports usage
347
+ * without touching the host; any other argument is trimmed of surrounding
348
+ * whitespace and used verbatim.
349
+ */
350
+ export function parseRestoreArtifactArgument(rawInput: string | undefined): string | undefined {
351
+ if (rawInput === undefined) return undefined;
352
+ const trimmed = rawInput.trim();
353
+ return trimmed.length === 0 ? undefined : trimmed;
354
+ }
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Windows-first experimental runtime defaults (Phase 2 wiring).
3
+ *
4
+ * These are the explicitly selected, owner-authorized experimental values
5
+ * recorded in `docs/v1/decision-log.md` (2026-10-11 entries) for a Windows
6
+ * experimental build only. They are NOT a production approval, not a format
7
+ * freeze, and not a general memory budget, compatibility, or support claim.
8
+ *
9
+ * - Crypto profile `sessionport-experimental-win-v1`: profile code 1,
10
+ * Argon2id13 `crypto_pwhash`, `opslimit=2`, `memlimit=128 MiB`, the current
11
+ * implementation's 16-byte random salt, 64-KiB plaintext frames, password cap
12
+ * 4 KiB, total encrypted artifact cap 66 MiB, decrypted gzip input cap
13
+ * 64 MiB, decompressed payload cap 256 MiB, expansion ratio 128 defined as
14
+ * decompressed payload bytes / decrypted compressed gzip bytes.
15
+ * - Collection bounds: session page size 50, message page size 200, at most
16
+ * 1,000 sessions, at most 10,000 messages per session, and at most 50,000
17
+ * messages total. Exceeding any bound fails the whole collection; nothing is
18
+ * ever truncated.
19
+ */
20
+
21
+ import type { CryptoPolicy } from "../../core/contracts/index.ts";
22
+ import type { CryptoPolicyDefinition } from "../../core/crypto/index.ts";
23
+ import type { SessionCollectionPolicy } from "../../core/sessions/index.ts";
24
+
25
+ /** Explicit allow-listed policy id for the Windows experimental profile. */
26
+ export const EXPERIMENTAL_CRYPTO_POLICY_ID = "sessionport-experimental-win-v1";
27
+
28
+ /** The explicit policy selector passed to the crypto factory; never read from an artifact. */
29
+ export const EXPERIMENTAL_CRYPTO_POLICY: CryptoPolicy = Object.freeze({
30
+ id: EXPERIMENTAL_CRYPTO_POLICY_ID,
31
+ });
32
+
33
+ /** Experimental password byte limit; the password UI is given the same value. */
34
+ export const EXPERIMENTAL_MAX_PASSWORD_BYTES = 4 * 1024;
35
+
36
+ /**
37
+ * The complete experimental allow-list definition. Numeric values are the
38
+ * owner-recorded Windows-experimental defaults; changing any value requires a
39
+ * new recorded decision and a fresh review (O-01/O-02/O-03 remain PARTIALLY
40
+ * RESOLVED for the experimental Windows build only).
41
+ */
42
+ export const EXPERIMENTAL_CRYPTO_POLICY_DEFINITION: CryptoPolicyDefinition = deepFreeze({
43
+ id: EXPERIMENTAL_CRYPTO_POLICY_ID,
44
+ profileCode: 1,
45
+ kdf: {
46
+ opsLimit: 2,
47
+ memLimit: 128 * 1024 * 1024,
48
+ },
49
+ compression: {
50
+ level: undefined,
51
+ },
52
+ framing: {
53
+ chunkBytes: 64 * 1024,
54
+ },
55
+ limits: {
56
+ maxPasswordBytes: EXPERIMENTAL_MAX_PASSWORD_BYTES,
57
+ maxArtifactBytes: 66 * 1024 * 1024,
58
+ maxCompressedBytes: 64 * 1024 * 1024,
59
+ maxDecompressedBytes: 256 * 1024 * 1024,
60
+ maxExpansionRatio: 128,
61
+ },
62
+ });
63
+
64
+ /**
65
+ * The complete experimental collection policy. `maxTotalMessages` is the
66
+ * required core aggregate cap (zero is a valid fail-closed value in core; the
67
+ * experimental value is 50,000).
68
+ */
69
+ export const EXPERIMENTAL_COLLECTION_POLICY: SessionCollectionPolicy = deepFreeze({
70
+ sessionPageSize: 50,
71
+ messagePageSize: 200,
72
+ maxSessions: 1000,
73
+ maxMessagesPerSession: 10000,
74
+ maxTotalMessages: 50000,
75
+ });
76
+
77
+ /** Deep-freezes a plain JSON-shaped configuration object. */
78
+ function deepFreeze<T>(value: T): T {
79
+ if (value !== null && typeof value === "object") {
80
+ for (const key of Object.keys(value)) {
81
+ deepFreeze((value as Record<string, unknown>)[key]);
82
+ }
83
+ Object.freeze(value);
84
+ }
85
+ return value;
86
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * SessionPort TUI wiring (Phase 2 experimental Windows lane).
3
+ *
4
+ * Public surface:
5
+ * - `installSessionPortWiring(context, options?)` — Windows-only install root:
6
+ * pinned-sodium initialization, real adapters/artifact I/O/password modal,
7
+ * command registration through the existing component-scoped seam, and the
8
+ * slot disposer returned unchanged.
9
+ * - `createSessionPortHandlers(dependencies)` — fully injected composition of
10
+ * the `/checkpoint` and `/restore` command callbacks (fake-based testable).
11
+ * - Experimental runtime defaults (crypto profile + collection bounds +
12
+ * password byte limit) and the sanitized report helpers.
13
+ *
14
+ * Boundaries: no shell/Git, no LLM/model call, no historical tool replay, no
15
+ * direct database access, no `project.update`; only already-sealed ciphertext
16
+ * is written, only encrypted artifacts are read, and every failure report is
17
+ * generic/sanitized. No production approval, format freeze, or platform-support
18
+ * claim is made (O-01/O-02/O-03 remain PARTIALLY RESOLVED for the experimental
19
+ * Windows build only; OQ-16 stays OPEN).
20
+ */
21
+
22
+ export {
23
+ createSessionPortHandlers,
24
+ parseCheckpointOutputArgument,
25
+ parseRestoreArtifactArgument,
26
+ } from "./composition.ts";
27
+ export type {
28
+ SessionPortHandlerDependencies,
29
+ SessionPortNotifier,
30
+ SessionPortPasswordProvider,
31
+ } from "./composition.ts";
32
+ export { installSessionPortWiring } from "./install.ts";
33
+ export type { SessionPortInstallOptions } from "./install.ts";
34
+ export {
35
+ EXPERIMENTAL_COLLECTION_POLICY,
36
+ EXPERIMENTAL_CRYPTO_POLICY,
37
+ EXPERIMENTAL_CRYPTO_POLICY_DEFINITION,
38
+ EXPERIMENTAL_CRYPTO_POLICY_ID,
39
+ EXPERIMENTAL_MAX_PASSWORD_BYTES,
40
+ } from "./experimental-profile.ts";
41
+ export {
42
+ reportCheckpointFailure,
43
+ reportCheckpointOutcome,
44
+ reportRestoreAuthenticationFailed,
45
+ reportRestoreCanceled,
46
+ reportRestoreCapabilityUnavailable,
47
+ reportRestoreExecution,
48
+ reportRestoreFailure,
49
+ reportRestoreUsage,
50
+ } from "./reports.ts";
51
+ export type { SessionPortNotification, SessionPortNotificationVariant } from "./reports.ts";
@@ -0,0 +1,108 @@
1
+ /// <reference types="node" />
2
+ /**
3
+ * SessionPort TUI install/composition root (Phase 2 experimental Windows wiring).
4
+ *
5
+ * `installSessionPortWiring` is the only host-facing composition entry:
6
+ *
7
+ * - **Windows-first experimental only.** On any non-`win32` platform it returns
8
+ * `undefined` immediately: no sodium initialization, no host-context access,
9
+ * no command registration, and no platform-support claim.
10
+ * - On Windows it initializes the pinned `libsodium-wrappers-sumo@0.8.4`
11
+ * runtime first (`await sodium.ready` plus primitive/constant validation
12
+ * through `createCheckpointCryptoFactory`; `createRemappedIdDeriver` also
13
+ * awaits/validates it) and fails closed: a rejected initialization aborts
14
+ * setup before any command is registered.
15
+ * - Native commands are registered through the existing component-scoped
16
+ * `installSessionPortCommands` (E39: the keymap layer must live inside the
17
+ * app-slot component scope). The slot disposer is returned unchanged so the
18
+ * host can clean up its contribution as the host contract permits; command
19
+ * handlers own their errors and report sanitized copy, so a handler never
20
+ * throws raw host data at the host.
21
+ * - Completed non-empty restore executions are presented through the
22
+ * designer-owned `showRestoreReportDialog` (aggregate/per-session facts only);
23
+ * empty results and dialog-display failures keep sanitized toast copy.
24
+ *
25
+ * This module statically imports the designer-owned UI modules (password modal
26
+ * and restore report dialog), which import the optional UI peers
27
+ * (`@opentui/solid`, `solid-js`). The entry (`src/tui.ts`) imports this module
28
+ * dynamically only after its `process.platform === "win32"` check, so
29
+ * non-Windows plugin setup never evaluates those modules.
30
+ *
31
+ * Test seams (platform/factory/deriver/password provider) exist only for
32
+ * fake-based tests; production calls use the real defaults. The real password
33
+ * provider is the designer-owned masked TUI modal, invoked with the
34
+ * experimental 4-KiB limit.
35
+ */
36
+
37
+ import type { Plugin } from "@opencode/plugin/tui";
38
+ import type { CheckpointCryptoFactory } from "../../core/contracts/index.ts";
39
+ import { createCheckpointCryptoFactory } from "../../core/crypto/index.ts";
40
+ import { createRemappedIdDeriver, type RemappedIdDeriver } from "../../core/restore/index.ts";
41
+ import { readEncryptedArtifact, resolveCheckpointOutputPath, writeEncryptedArtifact } from "../artifacts/files.ts";
42
+ import { requestPassword as showPasswordDialog } from "../ui/password-dialog.tsx";
43
+ import { showRestoreReportDialog } from "../ui/restore-report-dialog.tsx";
44
+ import { installSessionPortCommands } from "../ui/sessionport-commands.tsx";
45
+ import { createSessionPortHandlers, type SessionPortPasswordProvider } from "./composition.ts";
46
+ import { EXPERIMENTAL_CRYPTO_POLICY_DEFINITION } from "./experimental-profile.ts";
47
+
48
+ /** Test-only seams; production omits every field. */
49
+ export interface SessionPortInstallOptions {
50
+ /** Platform gate override for tests; defaults to `process.platform`. */
51
+ readonly platform?: string;
52
+ /** Crypto-factory seam; the default creates the experimental Windows profile and awaits pinned sodium. */
53
+ readonly createCryptoFactory?: () => Promise<CheckpointCryptoFactory>;
54
+ /** Remapped-ID deriver seam; the default awaits/validates pinned sodium. */
55
+ readonly createIdDeriver?: () => Promise<RemappedIdDeriver>;
56
+ /** Password-provider seam; the default is the designer-owned masked TUI modal. */
57
+ readonly requestPassword?: SessionPortPasswordProvider;
58
+ }
59
+
60
+ /**
61
+ * Installs the SessionPort commands for the experimental Windows lane and
62
+ * returns the slot disposer, or `undefined` when this platform is not
63
+ * registered. Fails closed (rejects) when the pinned runtime or the host
64
+ * command surface is unavailable; nothing is registered in that case.
65
+ */
66
+ export async function installSessionPortWiring(
67
+ context: Plugin.Context,
68
+ options: SessionPortInstallOptions = {},
69
+ ): Promise<(() => void) | undefined> {
70
+ // Windows-first experimental gate: no registration and no host access anywhere else.
71
+ if ((options.platform ?? process.platform) !== "win32") return undefined;
72
+
73
+ // Pinned-runtime initialization (fail closed): the default factory awaits
74
+ // `sodium.ready` and validates every primitive the provisional format needs;
75
+ // the deriver repeats the readiness/validation before registration.
76
+ const cryptoFactory = await (options.createCryptoFactory ?? createExperimentalCryptoFactory)();
77
+ const idDeriver = await (options.createIdDeriver ?? createRemappedIdDeriver)();
78
+
79
+ const handlers = createSessionPortHandlers({
80
+ client: context.client,
81
+ getActiveDirectory: () => context.location?.directory,
82
+ dialog: context.ui.dialog,
83
+ notify: (notification) => {
84
+ context.ui.toast.show({
85
+ title: notification.title,
86
+ message: notification.message,
87
+ variant: notification.variant,
88
+ });
89
+ },
90
+ showRestoreReport: (model) => showRestoreReportDialog(context, model),
91
+ requestPassword:
92
+ options.requestPassword ?? ((mode, maxPasswordBytes) => showPasswordDialog(context, mode, maxPasswordBytes)),
93
+ cryptoFactory,
94
+ idDeriver,
95
+ resolveCheckpointOutputPath,
96
+ writeEncryptedArtifact,
97
+ readEncryptedArtifact,
98
+ });
99
+
100
+ // E39: registration must stay inside the existing app-slot/keymap
101
+ // component scope; the slot disposer is returned unchanged.
102
+ return installSessionPortCommands(context, handlers);
103
+ }
104
+
105
+ /** Default factory: the experimental Windows profile over the pinned runtime. */
106
+ async function createExperimentalCryptoFactory(): Promise<CheckpointCryptoFactory> {
107
+ return createCheckpointCryptoFactory([EXPERIMENTAL_CRYPTO_POLICY_DEFINITION]);
108
+ }