@byok-sdk/client 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 (91) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +11 -0
  3. package/dist/adapters/claude/claude-adapter.d.ts +155 -0
  4. package/dist/adapters/claude/events.d.ts +91 -0
  5. package/dist/adapters/claude/permission-mapping.d.ts +142 -0
  6. package/dist/adapters/claude/process-client.d.ts +103 -0
  7. package/dist/adapters/claude/resolve-approval-mcp-bin.d.ts +45 -0
  8. package/dist/adapters/claude/resolve-bin.d.ts +27 -0
  9. package/dist/adapters/codex/codex-adapter.d.ts +97 -0
  10. package/dist/adapters/codex/events.d.ts +41 -0
  11. package/dist/adapters/codex/permission-mapping.d.ts +107 -0
  12. package/dist/adapters/codex/process-runner.d.ts +86 -0
  13. package/dist/adapters/codex/resolve-bin.d.ts +25 -0
  14. package/dist/adapters/pi/events.d.ts +58 -0
  15. package/dist/adapters/pi/permission-mapping.d.ts +46 -0
  16. package/dist/adapters/pi/pi-adapter.d.ts +29 -0
  17. package/dist/adapters/pi/resolve-bin.d.ts +44 -0
  18. package/dist/adapters/pi/rpc-client.d.ts +101 -0
  19. package/dist/bin/approval-mcp-server.d.ts +69 -0
  20. package/dist/bin/audit-log.d.ts +212 -0
  21. package/dist/bin/byok-agent.d.ts +2 -0
  22. package/dist/bin/byok-agent.js +12347 -0
  23. package/dist/bin/byok-agent.js.map +1 -0
  24. package/dist/bin/byok-approval-mcp.d.ts +2 -0
  25. package/dist/bin/byok-approval-mcp.js +498 -0
  26. package/dist/bin/byok-approval-mcp.js.map +1 -0
  27. package/dist/bin/commands/approvals.d.ts +31 -0
  28. package/dist/bin/commands/approve-reject.d.ts +29 -0
  29. package/dist/bin/commands/doctor.d.ts +15 -0
  30. package/dist/bin/commands/pair.d.ts +7 -0
  31. package/dist/bin/commands/runtimes.d.ts +7 -0
  32. package/dist/bin/commands/service.d.ts +54 -0
  33. package/dist/bin/commands/start.d.ts +21 -0
  34. package/dist/bin/commands/status.d.ts +25 -0
  35. package/dist/bin/commands/support-bundle.d.ts +7 -0
  36. package/dist/bin/commands/tasks.d.ts +27 -0
  37. package/dist/bin/commands/unpair.d.ts +124 -0
  38. package/dist/bin/commands/workspaces.d.ts +16 -0
  39. package/dist/bin/config.d.ts +41 -0
  40. package/dist/bin/control-client.d.ts +50 -0
  41. package/dist/bin/format.d.ts +67 -0
  42. package/dist/bin/runtime-probe.d.ts +34 -0
  43. package/dist/bin/tasks-view.d.ts +49 -0
  44. package/dist/daemon/approvals.d.ts +87 -0
  45. package/dist/daemon/auth-manager.d.ts +52 -0
  46. package/dist/daemon/blob-client.d.ts +22 -0
  47. package/dist/daemon/connection-manager.d.ts +502 -0
  48. package/dist/daemon/control-protocol.d.ts +299 -0
  49. package/dist/daemon/control-server.d.ts +61 -0
  50. package/dist/daemon/create-daemon.d.ts +364 -0
  51. package/dist/daemon/cursor-store.d.ts +30 -0
  52. package/dist/daemon/daemon-owner.d.ts +24 -0
  53. package/dist/daemon/deterministic-jitter.d.ts +20 -0
  54. package/dist/daemon/device-keys.d.ts +45 -0
  55. package/dist/daemon/device-proof-signer.d.ts +41 -0
  56. package/dist/daemon/environment.d.ts +96 -0
  57. package/dist/daemon/git-workspace-store.d.ts +49 -0
  58. package/dist/daemon/git-workspace.d.ts +80 -0
  59. package/dist/daemon/http-client.d.ts +8 -0
  60. package/dist/daemon/journal/journal.d.ts +342 -0
  61. package/dist/daemon/journal/sqlite-journal.d.ts +173 -0
  62. package/dist/daemon/journal/sqlite-support.d.ts +96 -0
  63. package/dist/daemon/journal/storage-policy.d.ts +368 -0
  64. package/dist/daemon/long-poll-transport.d.ts +142 -0
  65. package/dist/daemon/observer.d.ts +345 -0
  66. package/dist/daemon/operational-health.d.ts +87 -0
  67. package/dist/daemon/policy.d.ts +16 -0
  68. package/dist/daemon/progress-batcher.d.ts +31 -0
  69. package/dist/daemon/runtime-capabilities.d.ts +36 -0
  70. package/dist/daemon/session-workspace-store.d.ts +75 -0
  71. package/dist/daemon/store.d.ts +54 -0
  72. package/dist/daemon/task-runner.d.ts +862 -0
  73. package/dist/daemon/truth-memory-client.d.ts +94 -0
  74. package/dist/daemon/url.d.ts +60 -0
  75. package/dist/daemon/ws-transport.d.ts +110 -0
  76. package/dist/diagnostics/diagnostics.d.ts +114 -0
  77. package/dist/diagnostics/support-bundle.d.ts +81 -0
  78. package/dist/index.d.ts +42 -0
  79. package/dist/index.js +10056 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/lifecycle/create-service-lifecycle.d.ts +59 -0
  82. package/dist/lifecycle/exec-runner.d.ts +141 -0
  83. package/dist/lifecycle/launchd.d.ts +55 -0
  84. package/dist/lifecycle/service-types.d.ts +177 -0
  85. package/dist/lifecycle/systemd.d.ts +46 -0
  86. package/dist/lifecycle/winsw.d.ts +64 -0
  87. package/dist/types.d.ts +178 -0
  88. package/dist/util/async-queue.d.ts +45 -0
  89. package/dist/util/atomic-write.d.ts +86 -0
  90. package/dist/util/secure-dir.d.ts +124 -0
  91. package/package.json +66 -0
@@ -0,0 +1,59 @@
1
+ import { type LaunchdDeps } from './launchd';
2
+ import { type SystemdDeps } from './systemd';
3
+ import { type WinswDeps } from './winsw';
4
+ import type { ServiceDefinition, ServiceLifecycle } from './service-types';
5
+ export declare class UnsupportedServicePlatformError extends Error {
6
+ constructor(platform: string);
7
+ }
8
+ export interface CreateServiceLifecycleOptions {
9
+ /**
10
+ * Overrides `process.platform` — test-only seam for exercising a
11
+ * specific platform's generator/install logic from any host (combine
12
+ * with a mocked `deps.run`/`deps.fs`, since the real launchctl/systemctl/
13
+ * WinSW binaries obviously aren't present on the "wrong" OS).
14
+ */
15
+ platform?: NodeJS.Platform;
16
+ /** Platform-specific DI seams (mocked exec, mocked fs, mocked homedir/getuid) — only the fields matching the resolved platform are ever read. */
17
+ deps?: LaunchdDeps & SystemdDeps & WinswDeps;
18
+ }
19
+ /**
20
+ * Platform-dispatched entry point for M3-4's lifecycle API: manages the
21
+ * daemon as a background OS service via the platform's own idiomatic
22
+ * mechanism —
23
+ *
24
+ * - **macOS**: a launchd LaunchAgent (`launchd.ts`).
25
+ * - **Linux**: a systemd user unit (`systemd.ts`).
26
+ * - **Windows**: a WinSW-wrapped Windows Service (`winsw.ts`) — Node has no
27
+ * native SCM control-handler support in core, so a wrapper is used
28
+ * rather than hand-rolling the SCM protocol; see `winsw.ts`'s own doc
29
+ * comment for why WinSW specifically.
30
+ *
31
+ * Every implementation delegates crash-restart entirely to the OS
32
+ * supervisor (`KeepAlive`/`Restart=on-failure`/`<onfailure>`) — none of them
33
+ * runs an in-process supervisor loop.
34
+ *
35
+ * Deliberately does NOT try to auto-resolve `ServiceDefinition.program`'s
36
+ * `command`/`agentBin` from `import.meta.resolve`/`import.meta.url`-style
37
+ * introspection the way `adapters/pi/resolve-bin.ts` does for pi's
38
+ * optionalDependency. That pattern is a genuinely hazardous fit here: a
39
+ * relative path from THIS source file to `bin/byok-agent.ts` (`../bin/...`,
40
+ * since `lifecycle/` and `bin/` are sibling directories under `src/`) does
41
+ * NOT survive tsup's bundling unchanged — `src/index.ts` and
42
+ * `src/bin/byok-agent.ts` are two SEPARATE, independently-bundled tsup
43
+ * entries (see `tsup.config.ts`), so any code from `lifecycle/` ends up
44
+ * inlined into `dist/index.js` itself, whose OWN directory is `dist/`, not
45
+ * `dist/lifecycle/` — a path relative to "wherever this bundled code
46
+ * actually runs from" would need to be `./bin/byok-agent.js` there, the
47
+ * OPPOSITE of the `../bin/...` that's correct in unbundled `src/`. Guessing
48
+ * across that bundle boundary is exactly the class of hazard
49
+ * `templates/packaging/sea/README.md` had to empirically work around for
50
+ * pi's own resolution path. Rather than add a second such hazard, this
51
+ * module requires the caller to supply an explicit, already-resolved
52
+ * `program.command`/args (see `service-types.ts`'s `nodeAgentProgram` —
53
+ * still just a formatting convenience, not a resolution mechanism) — the
54
+ * `install`/`uninstall`/`service-*` CLI subcommands (`bin/commands/service.ts`)
55
+ * default `agentBin` to `process.argv[1]` instead, which Node always
56
+ * populates correctly with the actual script path being run regardless of
57
+ * how it was invoked, bundled or not.
58
+ */
59
+ export declare function createServiceLifecycle(def: ServiceDefinition, opts?: CreateServiceLifecycleOptions): ServiceLifecycle;
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Result of running an external command, regardless of its exit code.
3
+ * `code !== 0` is an ordinary, expected outcome for several callers in this
4
+ * module (e.g. `launchctl bootout` on a service that isn't currently
5
+ * loaded, `systemctl --user is-active` on an inactive unit) — it is NOT
6
+ * treated as a thrown error. See {@link Runner}'s own doc comment for what
7
+ * DOES reject.
8
+ */
9
+ export interface RunResult {
10
+ code: number;
11
+ stdout: string;
12
+ stderr: string;
13
+ }
14
+ /**
15
+ * Runs an external service-manager CLI (`launchctl`, `systemctl`, a
16
+ * WinSW-produced `.exe`, `sc.exe`) and resolves with its exit code +
17
+ * captured stdout/stderr — it deliberately does NOT reject just because the
18
+ * command exited non-zero (see {@link RunResult}'s doc comment: that is
19
+ * everyday signal for several callers here, not failure). It DOES reject
20
+ * for a genuine spawn failure (the executable itself couldn't be found/run
21
+ * at all, e.g. `ENOENT`) — see {@link defaultRunner}'s implementation for
22
+ * how the two are told apart. Callers that need "ran and returned 0 or
23
+ * throw" wrap this with {@link runOrThrow}.
24
+ *
25
+ * The DI seam every one of `launchd.ts`/`systemd.ts`/`winsw.ts` accepts
26
+ * (`LaunchdDeps.run`/`SystemdDeps.run`/`WinswDeps.run`) — this is what lets
27
+ * the install/uninstall/start/stop/status logic for all three platforms be
28
+ * unit-tested from any single host OS with a plain mock, per M3-4's own
29
+ * verification requirement, without ever shelling out for real in tests.
30
+ */
31
+ export type Runner = (command: string, args: string[]) => Promise<RunResult>;
32
+ /**
33
+ * Real implementation: `child_process.execFile`, never a shell
34
+ * (`exec`/`shell: true`) — the same reasoning as the pi adapter's own
35
+ * `detect()` (see `templates/packaging/sea/README.md`'s "Windows note" and
36
+ * `adapters/pi/pi-adapter.ts`): no shell-quoting hazard for a service name,
37
+ * config path, or WinSW install directory containing spaces or special
38
+ * characters. Every command this module ever invokes (`launchctl`,
39
+ * `systemctl`, a WinSW-produced `.exe`, `sc.exe`) is a genuine native
40
+ * executable, never a `.cmd`/`.bat` shell script, so the one real caveat of
41
+ * `execFile`-without-`shell` on Windows (a `.cmd`/`.bat` target can't be
42
+ * `CreateProcess`'d directly) never applies here.
43
+ *
44
+ * Node's `execFile` callback distinguishes two failure shapes on its error
45
+ * argument: when the target process ran and merely exited non-zero, `error.code`
46
+ * is that NUMERIC exit code; when the executable itself couldn't be spawned
47
+ * (e.g. `ENOENT`), `error.code` is an ERRNO STRING and there is no real exit
48
+ * code at all. This implementation resolves the first case as an ordinary
49
+ * {@link RunResult} (letting callers decide whether a given non-zero exit
50
+ * matters) and rejects only the second (a real inability to run the
51
+ * command at all, which every caller should hear about).
52
+ */
53
+ export declare const defaultRunner: Runner;
54
+ /**
55
+ * Runs `command`/`args` via `run` and throws a clear, labeled error if it
56
+ * exits non-zero — for the subset of calls across launchd/systemd/WinSW
57
+ * that must actually succeed for `install()`/`start()` to honestly report
58
+ * "the service is now running" (e.g. `launchctl bootstrap`, `systemctl
59
+ * start`, `winsw install`). Callers that instead want best-effort/tolerant
60
+ * semantics (e.g. "stop if running, no-op if already stopped") call `run`
61
+ * directly and ignore the result — see `launchd.ts`/`systemd.ts`/
62
+ * `winsw.ts`'s own `uninstall()`/`stop()` implementations.
63
+ */
64
+ export declare function runOrThrow(run: Runner, command: string, args: string[], label: string): Promise<RunResult>;
65
+ /**
66
+ * Describes what a "genuinely nothing to do" outcome looks like for a
67
+ * best-effort stop/uninstall step whose target may legitimately already be
68
+ * absent (not currently loaded/enabled/installed) — as opposed to a genuine
69
+ * failure (permission denied, service busy, a manager fault, the manager
70
+ * itself unreachable) that must NOT be treated the same way. See
71
+ * {@link isIdempotentAbsence}.
72
+ */
73
+ export interface IdempotentAbsence {
74
+ /** Exit codes that ALONE mean "already absent", independent of any stdout/stderr text (e.g. Windows's `ERROR_SERVICE_DOES_NOT_EXIST`, 1060). */
75
+ codes?: readonly number[];
76
+ /** Case-insensitive patterns checked against `stdout + "\n" + stderr`; a match means "already absent/not loaded/does not exist" for this platform's tool. */
77
+ patterns: readonly RegExp[];
78
+ /**
79
+ * Case-insensitive patterns checked against the SAME `stdout + "\n" +
80
+ * stderr` text, and consulted BEFORE `codes`/`patterns`: a match here
81
+ * means "this is a genuine failure", full stop, even if `codes`/`patterns`
82
+ * would otherwise call the result idempotent absence. Exists because a
83
+ * connectivity/permission/manager-unreachable error can be textually
84
+ * indistinguishable from — or reuse the very same generic OS errno string
85
+ * as — a genuine "not loaded"/"does not exist" message. Concretely: on a
86
+ * headless host with no reachable `systemd --user` D-Bus session (common
87
+ * for exactly the SSH/no-lingering boxes this daemon runs on),
88
+ * `systemctl --user disable --now` fails with `Failed to connect to bus:
89
+ * No such file or directory` — the SAME "No such file or directory" text
90
+ * a genuinely-absent unit can also produce, but here it means "we could
91
+ * not even ask the manager", not "the manager confirms it's gone".
92
+ * Conflating the two previously let this REAL failure look identical to
93
+ * idempotent absence and then delete the still-relevant plist/unit/exe
94
+ * (cross-model-review P1 #7, round 2 — the same re-orphan shape the
95
+ * original `codes`/`patterns` split was meant to fix in round 1). Each
96
+ * platform module supplies its own `neverAbsence` for the same reason it
97
+ * supplies its own `patterns`/`codes`: the actual wording is tool-specific
98
+ * — see `launchd.ts`/`systemd.ts`/`winsw.ts`'s own constants.
99
+ */
100
+ neverAbsence?: readonly RegExp[];
101
+ }
102
+ /**
103
+ * Distinguishes a KNOWN idempotent "already absent / not loaded / does not
104
+ * exist" outcome (safe to still proceed with cleanup) from a genuine
105
+ * failure. A non-zero exit is ambiguous on its own: `launchctl bootout`,
106
+ * `systemctl disable --now`, and a WinSW `stop`+`uninstall` all use the SAME
107
+ * non-zero exit for both "there was nothing to stop/unregister" (fine, an
108
+ * everyday outcome — see this module's own {@link RunResult} doc comment)
109
+ * and "I refused/failed to do it" (not fine) — conflating the two
110
+ * previously let a real failure look identical to success and then delete
111
+ * the plist/unit/exe out from under a still-running service
112
+ * (cross-model-review P1 #7: an orphaned, uncontrollable process). Each
113
+ * platform module supplies its own `patterns`/`codes` because the actual
114
+ * wording/codes are tool-specific — see `launchd.ts`/`systemd.ts`/
115
+ * `winsw.ts`'s own constants.
116
+ *
117
+ * `absence.neverAbsence` is checked FIRST (right after the unambiguous
118
+ * `code === 0` success case) and, on a match, wins outright over any
119
+ * `codes`/`patterns` match — see {@link IdempotentAbsence.neverAbsence}'s
120
+ * own doc comment for why a connectivity/permission/manager-unreachable
121
+ * signal must never be reclassified as absence no matter what else matches
122
+ * (cross-model-review P1 #7, round 2).
123
+ */
124
+ export declare function isIdempotentAbsence(result: RunResult, absence: IdempotentAbsence): boolean;
125
+ /**
126
+ * Runs `command`/`args` via `run` for a best-effort stop/uninstall step and
127
+ * throws a clear, labeled error UNLESS the result is either a genuine
128
+ * success or a known idempotent "already absent" outcome (see
129
+ * {@link isIdempotentAbsence}). Unlike {@link runOrThrow} (which throws on
130
+ * ANY non-zero exit, for calls that must actually succeed), this is the
131
+ * tolerant-but-not-blind form each platform's `uninstall()` needs: proceed
132
+ * to delete the plist/unit/exe+xml ONLY when this resolves without
133
+ * throwing; a thrown error here means "do NOT delete the control files —
134
+ * surface this to the caller so they can retry" (see each platform's own
135
+ * `uninstall()`). Each platform's standalone `stop()` uses the same
136
+ * function for the same reason: tolerate "already stopped/not loaded", but
137
+ * surface — rather than silently swallow — a genuine failure (permission
138
+ * denied, manager unreachable) instead of misreporting it as "stopped"
139
+ * (cross-model-review P1 #7, round 2, second half).
140
+ */
141
+ export declare function runIdempotent(run: Runner, command: string, args: string[], label: string, absence: IdempotentAbsence): Promise<RunResult>;
@@ -0,0 +1,55 @@
1
+ import { promises as fsp } from 'node:fs';
2
+ import { type Runner } from './exec-runner';
3
+ import { type ServiceDefinition, type ServiceLifecycle, type ServiceProgram } from './service-types';
4
+ /** DI seam for tests — see `exec-runner.ts`'s `Runner` doc comment for why a mocked `run` is enough to unit-test all of this file's install/uninstall/start/stop/status logic on any host OS. */
5
+ export interface LaunchdDeps {
6
+ run?: Runner;
7
+ fs?: Pick<typeof fsp, 'mkdir' | 'writeFile' | 'rm' | 'stat'>;
8
+ homedir?: () => string;
9
+ /** Defaults to `process.getuid` — macOS/Linux only; this module is never constructed on `win32` (see `create-service-lifecycle.ts`). */
10
+ getuid?: () => number;
11
+ }
12
+ /**
13
+ * Generates a launchd LaunchAgent plist for `label` running `program`,
14
+ * logging to `logDir`. Pure/no I/O — unit-tested directly for exact content
15
+ * shape.
16
+ *
17
+ * - `RunAtLoad`: true — starts immediately on `launchctl bootstrap`.
18
+ * - `KeepAlive.SuccessfulExit: false` — the standard, widely-documented
19
+ * launchd idiom for "restart only on crash/non-zero-or-signaled exit, do
20
+ * NOT restart after a clean `exit(0)`". This is the crash-restart M3-4
21
+ * asks for, delegated entirely to launchd — nothing in this SDK
22
+ * supervises the process itself.
23
+ * - `ThrottleInterval: 10` — matches the WinSW recipe's `onfailure delay="10
24
+ * sec"` (see `winsw.ts`) so a crash-looping process backs off at a
25
+ * comparable rate on every platform, rather than launchd's own default
26
+ * (which is already 10s, but left implicit is easy to mistake for
27
+ * "unthrottled").
28
+ * - `StandardOutPath`/`StandardErrorPath` under `logDir` — this is what M3-4
29
+ * asks for explicitly ("StandardOut/Error paths under storeDir"), unlike
30
+ * `systemd.ts`'s unit (which ALSO writes append-mode log files under
31
+ * `logDir` for cross-platform parity, even though systemd's own native
32
+ * idiom is the journal).
33
+ */
34
+ export declare function generateLaunchdPlist(def: {
35
+ label: string;
36
+ program: ServiceProgram;
37
+ logDir: string;
38
+ }): string;
39
+ /**
40
+ * macOS LaunchAgent lifecycle, built on the modern `launchctl` subcommand
41
+ * interface (`bootstrap`/`bootout`/`enable`/`kickstart`/`print`, targeting
42
+ * `gui/<uid>` — the per-user GUI domain LaunchAgents run in — rather than
43
+ * the legacy `load`/`unload`/`start`/`stop`).
44
+ *
45
+ * `start`/`stop` mapping (a real nuance worth documenting, not an arbitrary
46
+ * choice): `KeepAlive` means a plain signal-kill (`launchctl kill`) would
47
+ * just have launchd immediately restart the job — that's the crash-restart
48
+ * feature working exactly as designed, but it means "kill" can't implement
49
+ * a genuine "stop" on its own. `bootout` (fully unloading the job from the
50
+ * domain) is the only primitive that stops it without KeepAlive fighting
51
+ * back, so `stop()` = `bootout` and `start()` = `bootstrap` again (reload
52
+ * from the still-on-disk plist) — not `kickstart`, which only force-restarts
53
+ * an ALREADY-loaded job.
54
+ */
55
+ export declare function createLaunchdLifecycle(def: ServiceDefinition, deps?: LaunchdDeps): ServiceLifecycle;
@@ -0,0 +1,177 @@
1
+ /**
2
+ * What the OS service manager should actually execute to run the daemon in
3
+ * the background. Deliberately requires an explicit `command` (an absolute
4
+ * path is strongly recommended, not just a bare command name) rather than
5
+ * ever trying to auto-resolve one internally — see
6
+ * `create-service-lifecycle.ts`'s module doc comment for why. A bare
7
+ * command name relies on the OS service manager's own minimal PATH, which
8
+ * commonly does NOT include nvm/volta/homebrew node install directories —
9
+ * a frequent real-world "service can't find node" bug on every one of the
10
+ * three platforms this module supports.
11
+ */
12
+ export interface ServiceProgram {
13
+ command: string;
14
+ /**
15
+ * Args passed verbatim to `command` — e.g. `[agentBinPath, 'start',
16
+ * '--config', configPath]` for a plain node + script run (see
17
+ * {@link nodeAgentProgram}), or `['start', '--config', configPath]` alone
18
+ * if `command` is already a self-contained bundled binary (see
19
+ * `templates/packaging/`). Every platform generator passes these through
20
+ * untouched — `winsw.ts` emits one `<argument>` element per entry rather
21
+ * than a single shell-quoted string specifically so a path containing
22
+ * spaces never needs manual escaping (see that file's doc comment).
23
+ */
24
+ args: string[];
25
+ /** Working directory for the running service process. Each platform generator has its own documented default (see `launchd.ts`/`systemd.ts`/`winsw.ts`) when omitted. */
26
+ cwd?: string;
27
+ }
28
+ export interface NodeAgentProgramOptions {
29
+ /** Absolute path to the `byok-agent` entry script to run. No auto-detection — see `create-service-lifecycle.ts`'s doc comment. */
30
+ agentBin: string;
31
+ /** Absolute path to the JSON config file `byok-agent start` should load — resolve this to an absolute path BEFORE calling, since the service will run with the OS service manager's own cwd, not the caller's. */
32
+ configPath: string;
33
+ /** Node executable to invoke `agentBin` with. Defaults to `process.execPath` (the currently running node) — always an absolute, real path, unlike a bare `node` on PATH. */
34
+ nodeBin?: string;
35
+ cwd?: string;
36
+ }
37
+ /**
38
+ * Convenience builder for the common case: run `node <agentBin> start
39
+ * --config <configPath>` as the service's program. Still requires the
40
+ * caller to supply an absolute `agentBin` — this helper is a formatting
41
+ * convenience only, not a resolution mechanism.
42
+ */
43
+ export declare function nodeAgentProgram(opts: NodeAgentProgramOptions): ServiceProgram;
44
+ export interface ServiceDefinition {
45
+ /** Stable service identifier — becomes the launchd `Label`, the systemd unit's basename, and the WinSW `<id>` (sanitized per-platform via {@link sanitizeServiceName}). Typically `DaemonConfig.productId`. */
46
+ name: string;
47
+ /** Human-readable display name — systemd `Description=`, WinSW `<name>`/`<description>`. Defaults to `name` if omitted. */
48
+ displayName?: string;
49
+ /** What to run — see {@link ServiceProgram}. */
50
+ program: ServiceProgram;
51
+ /** Directory the service's stdout/stderr logs are written under (created if missing). On Windows this also doubles as the default WinSW install directory — see `windows.installDir`. */
52
+ logDir: string;
53
+ /**
54
+ * Windows/WinSW-only inputs. Required when actually constructing a
55
+ * lifecycle on `win32` (`create-service-lifecycle.ts` throws a clear
56
+ * error otherwise); ignored on macOS/Linux.
57
+ */
58
+ windows?: {
59
+ /**
60
+ * Absolute path to the product-bundled WinSW executable. Decision-6
61
+ * boundary: this SDK never bundles or downloads this binary itself —
62
+ * see `templates/service/winsw/README.md`. The lifecycle copies it
63
+ * into `installDir` under this service's own name (WinSW's own
64
+ * convention: the exe and its XML config must share a basename).
65
+ */
66
+ winswBin: string;
67
+ /** Directory the renamed WinSW exe + generated XML are installed into. Defaults to `logDir`. */
68
+ installDir?: string;
69
+ };
70
+ }
71
+ /** Options for {@link ServiceLifecycle.install} — an escape hatch for the rare case of reinstalling with a changed program (e.g. after an upgrade moved the agent binary) without reconstructing the whole lifecycle object. Omit to reuse the program given to `createServiceLifecycle`. */
72
+ export interface ServiceInstallOptions {
73
+ program?: ServiceProgram;
74
+ }
75
+ export interface ServiceStatusResult {
76
+ /** Whether the platform's own service manager has this service registered at all (a plist/unit/WinSW-config file present — checked directly, not inferred from `running`). */
77
+ installed: boolean;
78
+ /** Whether it's currently running, per the platform's own authoritative query (`launchctl print`, `systemctl --user is-active`, `sc.exe query`) — never a locally-cached guess. `false` here means "confirmed not running" ONLY when `determinate` is also `true` — see that field's own doc comment. */
79
+ running: boolean;
80
+ /**
81
+ * Finding P1 #2 (residual, round 3): whether `running`/`installed` above
82
+ * were actually CONFIRMED by a clean query, as opposed to a fallback
83
+ * because the platform's own service-manager tool could not be asked at
84
+ * all. `run()` (`exec-runner.ts`'s `Runner`) resolves an ordinary
85
+ * non-throwing `RunResult` for a bus-connect failure, an unreachable
86
+ * launchd GUI domain, or a permission-denied query — the SAME shape a
87
+ * genuine "not running" query returns — so without this field, a caller
88
+ * receiving `running: false` could not tell "the manager confirms it's
89
+ * not running" apart from "the manager could not even be asked" (both
90
+ * silently collapsed into the same boolean). Concretely:
91
+ *
92
+ * - `true`: either an authoritative "running"/"active" match, or a query
93
+ * that resolved with a KNOWN clean "not running"/"not loaded"/"not
94
+ * found" result.
95
+ * - `false`: the query itself could not be answered — a
96
+ * connectivity/permission/manager-unreachable failure (each platform
97
+ * classifies this with the SAME `neverAbsence`-style pattern list its
98
+ * `uninstall()`/`stop()` already use for the identical reason — see
99
+ * `systemd.ts`/`launchd.ts`/`winsw.ts`'s own
100
+ * `*_CONNECTIVITY_OR_PERMISSION_FAILURE` constants). `running: false`
101
+ * in this case is a FALLBACK, not a confirmed fact.
102
+ *
103
+ * Callers that must not fail open on an unreachable manager (see
104
+ * `bin/commands/unpair.ts`'s `checkServiceState`) must treat
105
+ * `determinate: false` exactly the same as a thrown `status()` call.
106
+ */
107
+ determinate: boolean;
108
+ /** Raw human-readable output from the underlying platform tool, for the `service-status` CLI subcommand and debugging. Never parsed further than the booleans above. */
109
+ detail: string;
110
+ }
111
+ /**
112
+ * The lifecycle API M3-4 asks for: `install(opts) / uninstall() / start() /
113
+ * stop() / status()`. Every method but `install` is deliberately
114
+ * parameterless — the service's identity/program/logDir are already fixed
115
+ * at `createServiceLifecycle(definition, ...)` construction time (mirrors
116
+ * `createDaemon(config)`'s own "5-line launcher" shape), so `uninstall`/
117
+ * `start`/`stop`/`status` always act on that one already-known service.
118
+ *
119
+ * Crash-restart is ALWAYS delegated to the OS supervisor (launchd
120
+ * `KeepAlive`, systemd `Restart=on-failure`, WinSW `<onfailure>`) — no
121
+ * implementation of this interface runs an in-process supervisor loop of
122
+ * its own.
123
+ *
124
+ * Idempotency convention shared by every platform implementation: `install`
125
+ * and `start` hard-fail (throw) if the final "make it actually running"
126
+ * step fails, since silently doing nothing there would misreport success.
127
+ * `stop` is best-effort/tolerant ONLY of a KNOWN idempotent "already
128
+ * stopped"/"not loaded"/"not installed" result (mirrors `Daemon.unpair()`'s
129
+ * own "safe to call at any point in the lifecycle" convention in
130
+ * `daemon/create-daemon.ts` for that idempotent case) — a genuine failure
131
+ * (permission denied, manager unreachable, the exe locked/busy) is thrown
132
+ * instead of being misreported as "stopped", using the same precise
133
+ * classifier `uninstall` uses (cross-model-review P1 #7, round 2, second
134
+ * half: silently swallowing a real `stop()` failure undermines the operator's
135
+ * ability to trust "stopped" at all). `uninstall` is tolerant ONLY of a
136
+ * KNOWN idempotent "not loaded"/"does not exist"/"already absent" result
137
+ * from its stop+unregister step (see `exec-runner.ts`'s
138
+ * `isIdempotentAbsence`) — a genuine failure (permission denied, service
139
+ * busy, manager error, manager UNREACHABLE — e.g. no reachable systemd
140
+ * `--user` D-Bus session or launchd GUI domain for this uid, textually
141
+ * indistinguishable from "already absent" unless explicitly excluded, see
142
+ * `exec-runner.ts`'s `IdempotentAbsence.neverAbsence`) is thrown instead,
143
+ * and the plist/unit/winsw exe+xml are deliberately left in place, so a
144
+ * still-running service never loses its control files and becomes an
145
+ * orphan nobody can stop/uninstall (cross-model-review P1 #7).
146
+ */
147
+ export interface ServiceLifecycle {
148
+ /** Writes the platform service definition and registers + starts it with the OS service manager. Safe to call again later (e.g. after an upgrade): overwrites the definition and reloads it. */
149
+ install(opts?: ServiceInstallOptions): Promise<void>;
150
+ /** Stops (if running) and fully removes the service registration + generated definition file. Safe to call when not installed. Throws — and leaves the control file in place — if the underlying service manager reports a genuine failure rather than success/"not installed"; see this interface's own doc comment. */
151
+ uninstall(): Promise<void>;
152
+ /** Starts an already-installed service. Throws a clear error if it isn't installed. */
153
+ start(): Promise<void>;
154
+ /** Stops a running service without uninstalling it. Safe to call when already stopped (or not installed/not loaded). Throws if the underlying service manager reports a genuine failure instead (e.g. permission denied, manager unreachable) rather than silently reporting success; see this interface's own doc comment. */
155
+ stop(): Promise<void>;
156
+ /** Current installed/running state, queried fresh from the platform's own service manager. */
157
+ status(): Promise<ServiceStatusResult>;
158
+ }
159
+ /**
160
+ * Sanitizes a free-form product/service identifier into something safe to
161
+ * embed in a launchd `Label`, a systemd unit filename, and a WinSW `<id>`
162
+ * (which doubles as a Windows service name AND a generated filename) — the
163
+ * intersection of all three platforms' safe-identifier rules is
164
+ * "letters, digits, `.`, `-`, `_`". Anything outside that set collapses to
165
+ * `-`. A LEADING `-` is then stripped even though `-` is itself an allowed
166
+ * character: `systemctl`'s argument parser (and, generally, any
167
+ * getopt-style CLI) mistakes a bare positional argument starting with `-`
168
+ * for an option rather than the service/unit name — e.g. `systemctl --user
169
+ * enable --now -foo.service` — which would otherwise misparse every one of
170
+ * `launchd.ts`/`systemd.ts`/`winsw.ts`'s own `run()` calls that pass this
171
+ * sanitized name straight through as a CLI argument (cross-model-review P1
172
+ * #8). Called independently by each platform module (not once centrally) so
173
+ * every one of `launchd.ts`/`systemd.ts`/`winsw.ts` stays correct even when
174
+ * used directly, without going through `createServiceLifecycle`'s
175
+ * dispatcher.
176
+ */
177
+ export declare function sanitizeServiceName(name: string): string;
@@ -0,0 +1,46 @@
1
+ import { promises as fsp } from 'node:fs';
2
+ import { type Runner } from './exec-runner';
3
+ import { type ServiceDefinition, type ServiceLifecycle, type ServiceProgram } from './service-types';
4
+ /** DI seam for tests — see `exec-runner.ts`'s `Runner` doc comment. */
5
+ export interface SystemdDeps {
6
+ run?: Runner;
7
+ fs?: Pick<typeof fsp, 'mkdir' | 'writeFile' | 'rm' | 'stat'>;
8
+ homedir?: () => string;
9
+ }
10
+ /**
11
+ * Generates a systemd user unit for `name` running `program`, logging to
12
+ * `logDir`. Pure/no I/O — unit-tested directly for exact content shape, and
13
+ * (locally, when `systemd-analyze` is available) verified with
14
+ * `systemd-analyze verify` — see `templates/service/systemd/README.md`.
15
+ *
16
+ * `Restart=on-failure` + `RestartSec=10` is the crash-restart M3-4 asks for,
17
+ * delegated entirely to systemd. `StandardOutput`/`StandardError` are
18
+ * pointed at append-mode files under `logDir` for parity with the launchd
19
+ * plist's `StandardOutPath`/`StandardErrorPath` and the WinSW `<logpath>` —
20
+ * systemd's own native idiom (the journal, `journalctl --user -u <name>`)
21
+ * still works unconditionally alongside this (systemd always journals
22
+ * unit's output; explicitly setting `StandardOutput=append:...` needs
23
+ * systemd >= 240, present on every currently-supported distro this SDK
24
+ * targets) and is documented in the README as the alternative.
25
+ */
26
+ export declare function generateSystemdUnit(def: {
27
+ name: string;
28
+ displayName: string;
29
+ program: ServiceProgram;
30
+ logDir: string;
31
+ }): string;
32
+ /**
33
+ * Linux systemd **user** service lifecycle (`~/.config/systemd/user/`,
34
+ * `systemctl --user ...`) — deliberately not a system-wide unit under
35
+ * `/etc/systemd/system/`, so install/uninstall never needs root, matching
36
+ * launchd's per-user LaunchAgent (not a system Daemon) and WinSW's
37
+ * per-machine Windows Service the same way each platform's own idiomatic
38
+ * "run this in the background for me" mechanism works.
39
+ *
40
+ * Requires a running systemd **user instance** for this user (normal on any
41
+ * desktop/login-manager session, and on modern systemd with
42
+ * `loginctl enable-linger` for a headless box) — a bare container with no
43
+ * systemd user session at all will fail every `systemctl --user` call here
44
+ * with a clear error surfaced from `runOrThrow`, not a silent no-op.
45
+ */
46
+ export declare function createSystemdLifecycle(def: ServiceDefinition, deps?: SystemdDeps): ServiceLifecycle;
@@ -0,0 +1,64 @@
1
+ import { promises as fsp } from 'node:fs';
2
+ import { type Runner } from './exec-runner';
3
+ import { type ServiceDefinition, type ServiceLifecycle, type ServiceProgram } from './service-types';
4
+ /** DI seam for tests — see `exec-runner.ts`'s `Runner` doc comment. */
5
+ export interface WinswDeps {
6
+ run?: Runner;
7
+ fs?: Pick<typeof fsp, 'mkdir' | 'writeFile' | 'rm' | 'stat' | 'copyFile'>;
8
+ }
9
+ /**
10
+ * Generates a WinSW (https://github.com/winsw/winsw) service descriptor XML
11
+ * for `id` running `program`, logging to `logDir`. Pure/no I/O — unit-tested
12
+ * directly for exact content shape; the REAL proof this is valid WinSW XML
13
+ * is the `windows-service-smoke` CI job (`.github/workflows/ci.yml`), which
14
+ * actually runs `winsw install` against generated output on a real
15
+ * `windows-latest` runner — this macOS dev box cannot execute WinSW at all.
16
+ *
17
+ * Each argument becomes its own `<argument>` element (WinSW's supported
18
+ * repeatable-element form) rather than a single space-joined `<arguments>`
19
+ * string — this sidesteps shell-style quoting entirely for a config path
20
+ * containing spaces, the exact same concern the pi adapter's
21
+ * `execFile`-without-`shell` note raises for Windows (see
22
+ * `templates/packaging/sea/README.md`'s "Windows note": WinSW itself
23
+ * launches `<executable>` directly, no shell involved, so an unescaped
24
+ * space in a single `<arguments>` string would be split in the wrong
25
+ * place — one `<argument>` per token has no such ambiguity).
26
+ *
27
+ * `<onfailure action="restart" delay="10 sec"/>` (with a second, longer
28
+ * backoff on repeated failure) is the crash-restart M3-4 asks for,
29
+ * delegated entirely to WinSW/the Windows SCM — nothing in this SDK
30
+ * supervises the process itself. `<startmode>Automatic</startmode>` mirrors
31
+ * launchd's `RunAtLoad`/systemd's `WantedBy=default.target`: the service
32
+ * also starts automatically on the next machine boot, not just right now.
33
+ */
34
+ export declare function generateWinswXml(def: {
35
+ id: string;
36
+ displayName: string;
37
+ program: ServiceProgram;
38
+ logDir: string;
39
+ }): string;
40
+ /**
41
+ * Windows Service lifecycle via WinSW — the standard, widely-used .NET
42
+ * service wrapper that gives any exe/command real Windows Service Control
43
+ * Manager (SCM) integration (crash-restart, logging, boot autostart)
44
+ * without this SDK hand-rolling the SCM protocol in Node (which has no
45
+ * native control-handler support in core — the reason a wrapper is needed
46
+ * at all).
47
+ *
48
+ * Decision-6 boundary: the product supplies the WinSW binary
49
+ * (`def.windows.winswBin`); this module only generates the correct config
50
+ * and drives install/uninstall/start/stop/status around it. WinSW's own
51
+ * convention is that its executable and XML config share a basename in the
52
+ * same directory (`<id>.exe` + `<id>.xml`), so `install()` copies the
53
+ * product-supplied binary into place under that name rather than invoking
54
+ * it in place — this is the version-agnostic approach documented across
55
+ * WinSW v2/v3, unlike relying on a specific `--config` CLI flag that may
56
+ * differ between major versions.
57
+ *
58
+ * `status()` queries `sc.exe` (the real Windows SCM query tool, always
59
+ * present) rather than parsing WinSW's own `status` subcommand text —
60
+ * authoritative ground truth independent of WinSW's own output, and the
61
+ * exact tool `templates/service/winsw/smoke-test.mjs` / the CI job also
62
+ * assert against.
63
+ */
64
+ export declare function createWinswLifecycle(def: ServiceDefinition, deps?: WinswDeps): ServiceLifecycle;