opencode-session-port 0.0.0-stage → 0.1.1

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 (56) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +84 -2
  3. package/dist/tui.js +10276 -0
  4. package/package.json +48 -4
  5. package/src/core/contracts/crypto.ts +66 -0
  6. package/src/core/contracts/index.ts +13 -0
  7. package/src/core/contracts/json.ts +142 -0
  8. package/src/core/contracts/session.ts +12 -0
  9. package/src/core/crypto/container.ts +686 -0
  10. package/src/core/crypto/envelope.ts +232 -0
  11. package/src/core/crypto/errors.ts +52 -0
  12. package/src/core/crypto/gzip-stream.ts +388 -0
  13. package/src/core/crypto/index.ts +13 -0
  14. package/src/core/crypto/kdf.ts +101 -0
  15. package/src/core/crypto/policy.ts +202 -0
  16. package/src/core/crypto/zero.ts +33 -0
  17. package/src/core/restore/identity.ts +202 -0
  18. package/src/core/restore/index.ts +24 -0
  19. package/src/core/restore/planner.ts +1122 -0
  20. package/src/core/sessions/collector.ts +323 -0
  21. package/src/core/sessions/errors.ts +37 -0
  22. package/src/core/sessions/guards.ts +11 -0
  23. package/src/core/sessions/index.ts +29 -0
  24. package/src/core/sessions/policy.ts +80 -0
  25. package/src/core/sessions/port.ts +88 -0
  26. package/src/core/sessions/serialize.ts +15 -0
  27. package/src/core/sessions/unsettled.ts +35 -0
  28. package/src/integrations/omo/index.ts +28 -0
  29. package/src/tui/adapters/host-client.ts +254 -0
  30. package/src/tui/adapters/index.ts +72 -0
  31. package/src/tui/adapters/location-resolver.ts +281 -0
  32. package/src/tui/adapters/project-destinations.ts +271 -0
  33. package/src/tui/adapters/session-id-inventory.ts +378 -0
  34. package/src/tui/adapters/session-import.ts +189 -0
  35. package/src/tui/adapters/session-read.ts +115 -0
  36. package/src/tui/artifacts/files.ts +285 -0
  37. package/src/tui/orchestration/checkpoint.ts +160 -0
  38. package/src/tui/orchestration/index.ts +55 -0
  39. package/src/tui/orchestration/restore-auth.ts +94 -0
  40. package/src/tui/orchestration/restore-execution.ts +381 -0
  41. package/src/tui/orchestration/restore-preflight.ts +223 -0
  42. package/src/tui/orchestration/shared.ts +75 -0
  43. package/src/tui/ui/password-dialog.tsx +251 -0
  44. package/src/tui/ui/password-flow.ts +288 -0
  45. package/src/tui/ui/restore-project-selector.ts +62 -0
  46. package/src/tui/ui/restore-report-dialog.tsx +251 -0
  47. package/src/tui/ui/restore-report-flow.ts +94 -0
  48. package/src/tui/ui/sessionport-commands.tsx +65 -0
  49. package/src/tui/wiring/composition.ts +354 -0
  50. package/src/tui/wiring/experimental-profile.ts +86 -0
  51. package/src/tui/wiring/index.ts +51 -0
  52. package/src/tui/wiring/install.ts +108 -0
  53. package/src/tui/wiring/reports.ts +340 -0
  54. package/src/tui.ts +34 -0
  55. package/tsconfig.json +18 -0
  56. package/tui.ts +17 -0
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Injected checkpoint flow (Phase 4, bounded orchestration; `fix-57`).
3
+ *
4
+ * `runCheckpointFlow` wires the completed Phase 2/3 core and Phase 4 adapters
5
+ * into one testable sequence and returns only low-level structured facts:
6
+ *
7
+ * collectCheckpointPayload(port, { projectID, policy })
8
+ * → zero sessions ⇒ `no-sessions` outcome (P-04), before the password
9
+ * prompt, path resolution, crypto creation, and any filesystem write
10
+ * → requestPassword("checkpoint")
11
+ * → cancel ⇒ `canceled` outcome, before path resolution and write
12
+ * → resolveCheckpointOutputPath({ projectDirectory, explicitPath? }) (P-15)
13
+ * → cryptoFactory.create(cryptoPolicy) (explicit injected policy only)
14
+ * → crypto.seal(asOneShotByteSource(serializeCheckpointPayload(payload)), password)
15
+ * → writeEncryptedArtifact(path, sealedSource)
16
+ *
17
+ * Password handling: the resolved `Uint8Array` is treated as flow-owned and is
18
+ * held until `writeEncryptedArtifact` has consumed the sealed iterable (the
19
+ * real container's KDF/framing may pull the plaintext lazily), then
20
+ * best-effort zeroed in `finally` on success and every failure path. There is
21
+ * no plaintext fallback, no plaintext temp file (A-03), and no secure-deletion
22
+ * promise (A-07).
23
+ *
24
+ * Truthful outcomes: only `no-sessions`, `canceled`, and `written` are
25
+ * returned. Collection, path, crypto, and write failures propagate unchanged
26
+ * (for example `SessionCollectionError`, `CryptoContainerError`, `EEXIST`,
27
+ * `ArtifactFileError`) so an error, cancel, or empty collection can never be
28
+ * reported as a created artifact. No O-15 report/UX format is produced here —
29
+ * the caller owns the final report.
30
+ *
31
+ * Policy boundaries (all still OPEN; nothing inferred):
32
+ * - O-01/O-02/O-03: no production numeric policy, format, or cap exists in
33
+ * this module. `CryptoPolicy` and the `CheckpointCryptoFactory` are injected
34
+ * by the caller; tests use clearly test-only fakes.
35
+ * - O-10: this flow uses the existing collector's project-scoped enumeration
36
+ * and adds no child/subagent enumeration of its own; child-association
37
+ * semantics remain OPEN and the collector includes exactly what the
38
+ * explicit project scope returns.
39
+ * - O-15: report formats remain OPEN; only path/session-count/encrypted-byte
40
+ * facts cross this boundary.
41
+ * - P-15/O-08: the path itself comes from the injected
42
+ * `resolveCheckpointOutputPath`; this module never invents a path and never
43
+ * overwrites an existing artifact.
44
+ *
45
+ * Evidence boundary: exercised only by fake ports, fake password providers,
46
+ * fake crypto factories, and fake store functions. No real host call,
47
+ * filesystem write, KDF, or import is performed by this lane, and no runtime
48
+ * or compatibility claim follows.
49
+ */
50
+
51
+ import {
52
+ collectCheckpointPayload,
53
+ serializeCheckpointPayload,
54
+ type SessionCollectionPolicy,
55
+ type SessionReadPort,
56
+ } from "../../core/sessions/index.ts";
57
+ import type { ByteSource, CheckpointCryptoFactory, CryptoPolicy } from "../../core/contracts/index.ts";
58
+ import type { ResolveCheckpointOutputPathOptions } from "../artifacts/files.ts";
59
+ import {
60
+ asOneShotByteSource,
61
+ isRecordValue,
62
+ requireCallable,
63
+ requireNonEmptyString,
64
+ zeroPasswordBestEffort,
65
+ type PasswordProvider,
66
+ } from "./shared.ts";
67
+
68
+ /** Explicit, fully injected checkpoint dependencies. Every field is required; no defaults exist. */
69
+ export interface CheckpointFlowInput {
70
+ /** Explicit project scope for collection; the collector re-validates it and never resolves "current". */
71
+ readonly projectID: string;
72
+ /** Current project directory used by the P-15 path policy (default `.sessionport/` location). */
73
+ readonly projectDirectory: string;
74
+ /** Explicit collection bounds; no defaults are applied (O-03). */
75
+ readonly policy: SessionCollectionPolicy;
76
+ /** Caller-supplied crypto factory; no production factory or profile is created here. */
77
+ readonly cryptoFactory: CheckpointCryptoFactory;
78
+ /** Explicit allow-listed policy for `factory.create`; never taken from an artifact. */
79
+ readonly cryptoPolicy: CryptoPolicy;
80
+ /** Injected `resolveCheckpointOutputPath` (P-15); the real function is passed by the product wiring. */
81
+ readonly resolveCheckpointOutputPath: (options: ResolveCheckpointOutputPathOptions) => Promise<string>;
82
+ /** Injected `writeEncryptedArtifact`; receives already-sealed ciphertext bytes only. */
83
+ readonly writeEncryptedArtifact: (targetPath: string, source: ByteSource) => Promise<number>;
84
+ /** Interactive password provider; called once per run, `undefined` means canceled. */
85
+ readonly requestPassword: PasswordProvider;
86
+ /** Optional explicit output-path override (P-15); omitted uses the default location. */
87
+ readonly explicitOutputPath?: string;
88
+ }
89
+
90
+ /** Low-level checkpoint outcome. No O-15 report format is implied. */
91
+ export type CheckpointOutcome =
92
+ /** P-04: the explicit project scope returned zero sessions; nothing was prompted, resolved, or written. */
93
+ | { readonly status: "no-sessions" }
94
+ /** The user canceled the password prompt; nothing was resolved, encrypted, or written. */
95
+ | { readonly status: "canceled" }
96
+ /** The sealed artifact was written; the facts below are raw observations, not a UX report. */
97
+ | {
98
+ readonly status: "written";
99
+ readonly artifactPath: string;
100
+ readonly sessionCount: number;
101
+ readonly encryptedBytes: number;
102
+ };
103
+
104
+ /**
105
+ * Runs one checkpoint to a completed encrypted artifact or to a clear
106
+ * no-sessions/canceled outcome. Errors propagate unchanged; see the module
107
+ * comment for the exact sequence and policy boundaries.
108
+ */
109
+ export async function runCheckpointFlow(
110
+ port: SessionReadPort,
111
+ input: CheckpointFlowInput,
112
+ ): Promise<CheckpointOutcome> {
113
+ if (!isRecordValue(input)) {
114
+ throw new TypeError(
115
+ "runCheckpointFlow requires an input object with explicit projectID, projectDirectory, policy, cryptoFactory, cryptoPolicy, resolveCheckpointOutputPath, writeEncryptedArtifact, and requestPassword",
116
+ );
117
+ }
118
+ requireCallable(input.requestPassword, "requestPassword");
119
+ requireCallable(input.resolveCheckpointOutputPath, "resolveCheckpointOutputPath");
120
+ requireCallable(input.writeEncryptedArtifact, "writeEncryptedArtifact");
121
+ if (!isRecordValue(input.cryptoFactory) || typeof input.cryptoFactory.create !== "function") {
122
+ throw new TypeError("runCheckpointFlow requires a CheckpointCryptoFactory with a create(policy) function");
123
+ }
124
+ const projectID = requireNonEmptyString(input.projectID, "projectID");
125
+ requireNonEmptyString(input.projectDirectory, "projectDirectory");
126
+
127
+ const payload = await collectCheckpointPayload(port, { projectID, policy: input.policy });
128
+ if (payload.sessions.length === 0) {
129
+ // P-04: no empty checkpoints. Nothing is prompted, resolved, encrypted, or written.
130
+ return { status: "no-sessions" };
131
+ }
132
+
133
+ const password = await input.requestPassword("checkpoint");
134
+ if (password === undefined) {
135
+ return { status: "canceled" };
136
+ }
137
+ if (!(password instanceof Uint8Array)) {
138
+ throw new TypeError("The checkpoint password provider must resolve with Uint8Array bytes or undefined");
139
+ }
140
+
141
+ try {
142
+ const artifactPath = await input.resolveCheckpointOutputPath({
143
+ projectDirectory: input.projectDirectory,
144
+ ...(input.explicitOutputPath !== undefined ? { explicitPath: input.explicitOutputPath } : {}),
145
+ });
146
+ const crypto = await input.cryptoFactory.create(input.cryptoPolicy);
147
+ // `serializeCheckpointPayload` returns bytes; `seal` consumes a ByteSource,
148
+ // so the serialized payload is handed over as a one-shot borrowed chunk.
149
+ const sealed = await crypto.seal(asOneShotByteSource(serializeCheckpointPayload(payload)), password);
150
+ const encryptedBytes = await input.writeEncryptedArtifact(artifactPath, sealed);
151
+ if (typeof encryptedBytes !== "number" || !Number.isSafeInteger(encryptedBytes) || encryptedBytes < 0) {
152
+ throw new TypeError("writeEncryptedArtifact must resolve with the exact non-negative integer byte count written");
153
+ }
154
+ return { status: "written", artifactPath, sessionCount: payload.sessions.length, encryptedBytes };
155
+ } finally {
156
+ // Best-effort only (A-07): the password is zeroed after the sealed
157
+ // iterable has been consumed (or the write failed), never before.
158
+ zeroPasswordBestEffort(password);
159
+ }
160
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * SessionPort V1 TUI orchestration (Phase 4, bounded non-visual lane; `fix-57`).
3
+ *
4
+ * Public surface:
5
+ * - `runCheckpointFlow(port, input)` → collect, prompt, resolve the P-15 path,
6
+ * seal, and write one encrypted artifact; returns only `no-sessions` /
7
+ * `canceled` / `written` low-level facts.
8
+ * - `runRestorePreflight(client, input)` → O-12 capability probe, one password
9
+ * attempt, authenticated strict-parse of the artifact, destination snapshot,
10
+ * the required bounded metadata-only cross-project session-ID occupancy
11
+ * inventory (owner-approved 2026-10-14 amendment; after auth/parse and the
12
+ * destination read, before planning), and a completed `planRestore`; returns
13
+ * the plan plus the caller's explicit target directory.
14
+ * - `runRestorePreflightWithPasswordAttempts(client, input)` → the
15
+ * owner-decided three-attempt restore password loop around
16
+ * `runRestorePreflight`; retries only the stable `authentication-failed`
17
+ * code and returns a terminal `authentication-failed` outcome after the
18
+ * permitted attempts instead of rethrowing the raw crypto error.
19
+ * - `runRestoreExecution(input)` → sequential parent-first execution of one
20
+ * completed `RestorePlan` through the injected `SessionImportAdapter` with
21
+ * truthful per-session outcomes (imported / skipped / failed / blocked /
22
+ * not-attempted), no retries, independent-branch continuation, blocked
23
+ * descendants of a failed/blocked/unattempted parent, and the caller's
24
+ * explicit target on every import (O-06 handling policy; the final report
25
+ * format stays OPEN as O-15).
26
+ *
27
+ * This lane performs no command registration, no real host calls, and no
28
+ * production crypto configuration. All effects are injected; tests use fake
29
+ * ports, fake clients, fake importers, fake crypto, and fake store functions
30
+ * only.
31
+ *
32
+ * OPEN decision boundaries preserved here: O-01/O-02/O-03 (no production
33
+ * profile, format, or numeric cap), O-06 (owner handling policy implemented;
34
+ * runtime partial-import behavior remains unvalidated), O-10 (child-session
35
+ * enumeration: the checkpoint flow adds none), O-11 (damage policy), O-15
36
+ * (final error taxonomy/report format: none is produced here), O-16 (the
37
+ * restore caller supplies an explicit target). No runtime, compatibility, or
38
+ * security claim follows from this module.
39
+ */
40
+
41
+ export { runCheckpointFlow } from "./checkpoint.ts";
42
+ export type { CheckpointFlowInput, CheckpointOutcome } from "./checkpoint.ts";
43
+ export { runRestorePreflight } from "./restore-preflight.ts";
44
+ export type { RestorePreflightInput, RestorePreflightOutcome } from "./restore-preflight.ts";
45
+ export { isAuthenticationFailure, RESTORE_PASSWORD_ATTEMPT_LIMIT, runRestorePreflightWithPasswordAttempts } from "./restore-auth.ts";
46
+ export type { RestoreAuthenticatedPreflightOutcome } from "./restore-auth.ts";
47
+ export { runRestoreExecution } from "./restore-execution.ts";
48
+ export type {
49
+ RestoreExecutionInput,
50
+ RestoreExecutionResult,
51
+ RestoreExecutionSummary,
52
+ RestoreSessionExecutionStatus,
53
+ RestoreSessionOutcome,
54
+ } from "./restore-execution.ts";
55
+ export type { PasswordProvider, PasswordPurpose } from "./shared.ts";
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Owner-decided restore password-attempt loop (Phase 2 experimental wiring).
3
+ *
4
+ * `runRestorePreflightWithPasswordAttempts` wraps the injected
5
+ * `runRestorePreflight` (Phase 4 orchestration) with the owner decision
6
+ * recorded in O-07/P-13 (2026-10-10): a restore permits **three failed
7
+ * password attempts per command run**.
8
+ *
9
+ * Exact semantics:
10
+ * - Each attempt calls `runRestorePreflight` exactly once, which prompts for
11
+ * the password through the injected provider and performs the full
12
+ * capability probe, lazy artifact read, authentication, strict parse,
13
+ * destination snapshot, the required bounded metadata-only cross-project
14
+ * session-ID occupancy inventory (owner-approved 2026-10-14 amendment), and
15
+ * the `planRestore` preflight.
16
+ * - Only an error carrying the stable crypto code `"authentication-failed"` is
17
+ * retried. The classification is structural (`error.code`), never based on
18
+ * message text, so no raw exception detail is inspected, exposed, or logged.
19
+ * - After the final permitted attempt, the outcome is a truthful
20
+ * `{ status: "authentication-failed" }`; the raw error is not rethrown, so
21
+ * the caller reports generic copy. This deliberately does not distinguish a
22
+ * wrong password from a damaged artifact.
23
+ * - Every other error (truncated/tampered artifact, invalid UTF-8/JSON,
24
+ * destination/planner failure, missing capability, provider failure)
25
+ * propagates unchanged on its first occurrence; nothing is retried.
26
+ * - `canceled` and `capability-unavailable` outcomes pass through unchanged and
27
+ * are never retried.
28
+ *
29
+ * Password ownership stays with `runRestorePreflight`: each attempt's resolved
30
+ * bytes are best-effort zeroed in its `finally` block; this wrapper never
31
+ * retains a password. No import is performed here; the caller owns any later
32
+ * `runRestoreExecution`.
33
+ *
34
+ * Evidence boundary: fake clients, fake crypto, fake artifact sources, fake
35
+ * destination callbacks, and a fake cross-project inventory callback only. No
36
+ * real host call, artifact read, KDF, or import is performed by this lane, and
37
+ * no runtime or compatibility claim follows.
38
+ */
39
+
40
+ import {
41
+ runRestorePreflight,
42
+ type RestorePreflightInput,
43
+ type RestorePreflightOutcome,
44
+ } from "./restore-preflight.ts";
45
+ import { isRecordValue } from "./shared.ts";
46
+
47
+ /**
48
+ * Owner-decided per-command-run restore attempt limit (O-07/P-13,
49
+ * 2026-10-10): three failed password attempts, then stop.
50
+ */
51
+ export const RESTORE_PASSWORD_ATTEMPT_LIMIT = 3;
52
+
53
+ /**
54
+ * Restore-preflight outcome including the terminal three-attempt
55
+ * authentication failure. The final case is returned instead of rethrowing the
56
+ * raw crypto error so callers can report generic copy.
57
+ */
58
+ export type RestoreAuthenticatedPreflightOutcome =
59
+ | RestorePreflightOutcome
60
+ | { readonly status: "authentication-failed" };
61
+
62
+ /** True only for the stable crypto `authentication-failed` code; never inspects message text. */
63
+ export function isAuthenticationFailure(error: unknown): boolean {
64
+ return isRecordValue(error) && error["code"] === "authentication-failed";
65
+ }
66
+
67
+ /**
68
+ * Runs the restore preflight with up to `maxAttempts` password attempts
69
+ * (default: the owner-decided three). See the module comment for the exact
70
+ * retry/classification policy.
71
+ */
72
+ export async function runRestorePreflightWithPasswordAttempts(
73
+ client: unknown,
74
+ input: RestorePreflightInput,
75
+ maxAttempts: number = RESTORE_PASSWORD_ATTEMPT_LIMIT,
76
+ ): Promise<RestoreAuthenticatedPreflightOutcome> {
77
+ if (typeof maxAttempts !== "number" || !Number.isSafeInteger(maxAttempts) || maxAttempts < 1) {
78
+ throw new TypeError("maxAttempts must be an explicit positive safe integer");
79
+ }
80
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
81
+ try {
82
+ return await runRestorePreflight(client, input);
83
+ } catch (error) {
84
+ if (!isAuthenticationFailure(error)) throw error;
85
+ if (attempt === maxAttempts) {
86
+ // Give up after the permitted attempts without disclosing the raw
87
+ // crypto error; the caller reports generic copy.
88
+ return { status: "authentication-failed" };
89
+ }
90
+ }
91
+ }
92
+ // Unreachable: the loop returns or throws on every iteration.
93
+ return { status: "authentication-failed" };
94
+ }