@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.
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/dist/adapters/claude/claude-adapter.d.ts +155 -0
- package/dist/adapters/claude/events.d.ts +91 -0
- package/dist/adapters/claude/permission-mapping.d.ts +142 -0
- package/dist/adapters/claude/process-client.d.ts +103 -0
- package/dist/adapters/claude/resolve-approval-mcp-bin.d.ts +45 -0
- package/dist/adapters/claude/resolve-bin.d.ts +27 -0
- package/dist/adapters/codex/codex-adapter.d.ts +97 -0
- package/dist/adapters/codex/events.d.ts +41 -0
- package/dist/adapters/codex/permission-mapping.d.ts +107 -0
- package/dist/adapters/codex/process-runner.d.ts +86 -0
- package/dist/adapters/codex/resolve-bin.d.ts +25 -0
- package/dist/adapters/pi/events.d.ts +58 -0
- package/dist/adapters/pi/permission-mapping.d.ts +46 -0
- package/dist/adapters/pi/pi-adapter.d.ts +29 -0
- package/dist/adapters/pi/resolve-bin.d.ts +44 -0
- package/dist/adapters/pi/rpc-client.d.ts +101 -0
- package/dist/bin/approval-mcp-server.d.ts +69 -0
- package/dist/bin/audit-log.d.ts +212 -0
- package/dist/bin/byok-agent.d.ts +2 -0
- package/dist/bin/byok-agent.js +12347 -0
- package/dist/bin/byok-agent.js.map +1 -0
- package/dist/bin/byok-approval-mcp.d.ts +2 -0
- package/dist/bin/byok-approval-mcp.js +498 -0
- package/dist/bin/byok-approval-mcp.js.map +1 -0
- package/dist/bin/commands/approvals.d.ts +31 -0
- package/dist/bin/commands/approve-reject.d.ts +29 -0
- package/dist/bin/commands/doctor.d.ts +15 -0
- package/dist/bin/commands/pair.d.ts +7 -0
- package/dist/bin/commands/runtimes.d.ts +7 -0
- package/dist/bin/commands/service.d.ts +54 -0
- package/dist/bin/commands/start.d.ts +21 -0
- package/dist/bin/commands/status.d.ts +25 -0
- package/dist/bin/commands/support-bundle.d.ts +7 -0
- package/dist/bin/commands/tasks.d.ts +27 -0
- package/dist/bin/commands/unpair.d.ts +124 -0
- package/dist/bin/commands/workspaces.d.ts +16 -0
- package/dist/bin/config.d.ts +41 -0
- package/dist/bin/control-client.d.ts +50 -0
- package/dist/bin/format.d.ts +67 -0
- package/dist/bin/runtime-probe.d.ts +34 -0
- package/dist/bin/tasks-view.d.ts +49 -0
- package/dist/daemon/approvals.d.ts +87 -0
- package/dist/daemon/auth-manager.d.ts +52 -0
- package/dist/daemon/blob-client.d.ts +22 -0
- package/dist/daemon/connection-manager.d.ts +502 -0
- package/dist/daemon/control-protocol.d.ts +299 -0
- package/dist/daemon/control-server.d.ts +61 -0
- package/dist/daemon/create-daemon.d.ts +364 -0
- package/dist/daemon/cursor-store.d.ts +30 -0
- package/dist/daemon/daemon-owner.d.ts +24 -0
- package/dist/daemon/deterministic-jitter.d.ts +20 -0
- package/dist/daemon/device-keys.d.ts +45 -0
- package/dist/daemon/device-proof-signer.d.ts +41 -0
- package/dist/daemon/environment.d.ts +96 -0
- package/dist/daemon/git-workspace-store.d.ts +49 -0
- package/dist/daemon/git-workspace.d.ts +80 -0
- package/dist/daemon/http-client.d.ts +8 -0
- package/dist/daemon/journal/journal.d.ts +342 -0
- package/dist/daemon/journal/sqlite-journal.d.ts +173 -0
- package/dist/daemon/journal/sqlite-support.d.ts +96 -0
- package/dist/daemon/journal/storage-policy.d.ts +368 -0
- package/dist/daemon/long-poll-transport.d.ts +142 -0
- package/dist/daemon/observer.d.ts +345 -0
- package/dist/daemon/operational-health.d.ts +87 -0
- package/dist/daemon/policy.d.ts +16 -0
- package/dist/daemon/progress-batcher.d.ts +31 -0
- package/dist/daemon/runtime-capabilities.d.ts +36 -0
- package/dist/daemon/session-workspace-store.d.ts +75 -0
- package/dist/daemon/store.d.ts +54 -0
- package/dist/daemon/task-runner.d.ts +862 -0
- package/dist/daemon/truth-memory-client.d.ts +94 -0
- package/dist/daemon/url.d.ts +60 -0
- package/dist/daemon/ws-transport.d.ts +110 -0
- package/dist/diagnostics/diagnostics.d.ts +114 -0
- package/dist/diagnostics/support-bundle.d.ts +81 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +10056 -0
- package/dist/index.js.map +1 -0
- package/dist/lifecycle/create-service-lifecycle.d.ts +59 -0
- package/dist/lifecycle/exec-runner.d.ts +141 -0
- package/dist/lifecycle/launchd.d.ts +55 -0
- package/dist/lifecycle/service-types.d.ts +177 -0
- package/dist/lifecycle/systemd.d.ts +46 -0
- package/dist/lifecycle/winsw.d.ts +64 -0
- package/dist/types.d.ts +178 -0
- package/dist/util/async-queue.d.ts +45 -0
- package/dist/util/atomic-write.d.ts +86 -0
- package/dist/util/secure-dir.d.ts +124 -0
- 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;
|