@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,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* M4 Phase 3: the testable core of `byok-approval-mcp` (`byok-approval-mcp.ts`
|
|
3
|
+
* is the thin stdio-wiring entry point — mirrors this repo's existing
|
|
4
|
+
* `bin/commands/*.ts` split: real logic lives in a plain module, the bin
|
|
5
|
+
* script itself is glue no test ever imports directly).
|
|
6
|
+
*
|
|
7
|
+
* `byok-approval-mcp` is the MCP stdio server `claude`'s own
|
|
8
|
+
* `--permission-prompt-tool` spawns AS ITS OWN CHILD PROCESS when the claude
|
|
9
|
+
* adapter runs a task under `PermissionPolicy.mode: 'confirm'` (see
|
|
10
|
+
* `../adapters/claude/permission-mapping.ts`'s `confirm`-mode doc comment).
|
|
11
|
+
* It implements just enough of the MCP stdio transport (JSON-RPC 2.0,
|
|
12
|
+
* newline-delimited, per the spec) to expose ONE tool — empirically
|
|
13
|
+
* confirmed end-to-end against the real installed claude 2.1.216 binary
|
|
14
|
+
* (M4 Phase 3 STEP 0): `initialize` -> `notifications/initialized` ->
|
|
15
|
+
* `tools/list` -> `tools/call`, with the tool's arguments shaped exactly
|
|
16
|
+
* `{tool_name, input, tool_use_id}` (claude's own real wire shape,
|
|
17
|
+
* live-captured) and its expected response shaped
|
|
18
|
+
* `{content:[{type:'text', text: JSON.stringify({behavior:'allow',
|
|
19
|
+
* updatedInput} | {behavior:'deny', message})}]}` — the SAME shape the
|
|
20
|
+
* Claude Agent SDK's in-process `canUseTool` callback returns (see
|
|
21
|
+
* platform.claude.com/docs/en/agent-sdk/user-input), just crossing a
|
|
22
|
+
* process boundary via MCP instead of an in-process function call.
|
|
23
|
+
*/
|
|
24
|
+
export declare const APPROVAL_TOOL_NAME = "approval_prompt";
|
|
25
|
+
/** Bound on how much of a tool call's `input` gets folded into the wire `task.await_approval.summary` — mirrors `events.ts`'s `RESULT_DIAGNOSTIC_MAX_CHARS`/`truncateResultDiagnostic` convention: a human-facing summary, not a full audit record. */
|
|
26
|
+
export declare const APPROVAL_SUMMARY_MAX_CHARS = 500;
|
|
27
|
+
/** `${toolName}: ${input}`, bounded — the human-readable description carried into `task.await_approval.summary` and (via the daemon/CLI's own rendering) whatever a real approver actually reads before deciding. */
|
|
28
|
+
export declare function summarizeToolCall(toolName: string, input: unknown): string;
|
|
29
|
+
export interface ApprovalOutcome {
|
|
30
|
+
approved: boolean;
|
|
31
|
+
reason?: string;
|
|
32
|
+
}
|
|
33
|
+
/** What `byok-approval-mcp.ts` (the real entry point) injects — the one real dependency this module has on the outside world. */
|
|
34
|
+
export interface ApprovalMcpDeps {
|
|
35
|
+
/** Requests a decision from the daemon this task is running on. Any rejection/throw here is treated as fail-closed (deny) by `handleMcpRequest` — never surfaced to claude as a raw protocol error. */
|
|
36
|
+
requestApproval(taskId: string, summary: string): Promise<ApprovalOutcome>;
|
|
37
|
+
}
|
|
38
|
+
interface JsonRpcRequestLike {
|
|
39
|
+
jsonrpc?: unknown;
|
|
40
|
+
id?: unknown;
|
|
41
|
+
method?: unknown;
|
|
42
|
+
params?: unknown;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Handles exactly one already-parsed JSON-RPC request object and returns the
|
|
46
|
+
* exact response object to write back (`undefined` for a notification that
|
|
47
|
+
* expects no reply, e.g. `notifications/initialized`) — no stdio/process
|
|
48
|
+
* concerns at all, so tests call this directly with a stub {@link
|
|
49
|
+
* ApprovalMcpDeps} instead of spawning a real process or a real control
|
|
50
|
+
* socket. `serveApprovalMcpOverStdio` below is the only caller in production.
|
|
51
|
+
*/
|
|
52
|
+
export declare function handleMcpRequest(req: JsonRpcRequestLike, deps: ApprovalMcpDeps, taskId: string): Promise<Record<string, unknown> | undefined>;
|
|
53
|
+
export interface ServeApprovalMcpOptions {
|
|
54
|
+
taskId: string;
|
|
55
|
+
deps: ApprovalMcpDeps;
|
|
56
|
+
/** Defaults to `process.stdin`/`process.stdout` — overridable so tests can drive this over in-memory streams. */
|
|
57
|
+
input?: NodeJS.ReadableStream;
|
|
58
|
+
output?: NodeJS.WritableStream;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Wires {@link handleMcpRequest} to real NDJSON stdio (the MCP stdio
|
|
62
|
+
* transport: one JSON-RPC message per line, both directions) — the only
|
|
63
|
+
* genuinely process-shaped piece of this module. Each line is handled
|
|
64
|
+
* independently and asynchronously (never serialized against the others),
|
|
65
|
+
* since claude's own parallel-tool-use can legitimately fire more than one
|
|
66
|
+
* concurrent `tools/call` over the same connection.
|
|
67
|
+
*/
|
|
68
|
+
export declare function serveApprovalMcpOverStdio(opts: ServeApprovalMcpOptions): void;
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import { type DaemonEvent, type DaemonEventListener } from '../index';
|
|
2
|
+
/**
|
|
3
|
+
* The audit log is the ONLY channel a separate, short-lived CLI invocation
|
|
4
|
+
* (`status`/`tasks`/`tasks --follow`) has into what an already-running
|
|
5
|
+
* `byok-agent start` process has observed — see `byok-agent.ts`'s header
|
|
6
|
+
* comment for the full read-model rationale. One JSON line per
|
|
7
|
+
* `DaemonEvent`, oldest first, append-only.
|
|
8
|
+
*
|
|
9
|
+
* Finding P1 #3 (SECURITY): a `DaemonEvent` can carry a task's raw
|
|
10
|
+
* instruction-derived output — `tool_use.input`/`tool_result.output` are
|
|
11
|
+
* `z.unknown()` in `@byok-sdk/protocol` (a shell command, a whole file's
|
|
12
|
+
* contents echoed back, anything), `progress`/`needs_approval`/`completed`/
|
|
13
|
+
* `failed`/`cancelled` all carry free-form agent/operator text, and
|
|
14
|
+
* `artifact.inline` is literally base64 file bytes. Persisting all of that
|
|
15
|
+
* VERBATIM into a durable, append-only, potentially long-retained file —
|
|
16
|
+
* previously created at the default `0666 & umask` mode (NOT 0600), with
|
|
17
|
+
* `storeDir` only chmod'd 0700 at the moment it's first created (a
|
|
18
|
+
* pre-existing custom `storeDir` kept whatever mode it already had) — is
|
|
19
|
+
* exactly the kind of thing that turns "helpful audit trail" into "durable
|
|
20
|
+
* secret/credential/source leak" the moment anything else on the machine can
|
|
21
|
+
* read it. Fixed on two independent axes:
|
|
22
|
+
*
|
|
23
|
+
* 1. **Redaction** (`redactForAudit`/`redactAgentEvent`): only event type,
|
|
24
|
+
* taskId, timestamps, tool/runtime NAMES, sizes/counts, and closed-enum
|
|
25
|
+
* state fields are ever written to disk — every free-form text/bytes
|
|
26
|
+
* field is replaced with its byte SIZE. `readAuditEvents`/`followAuditLog`
|
|
27
|
+
* reconstruct a `DaemonEvent`-shaped value from that redacted projection
|
|
28
|
+
* for their callers (`tasks-view.ts`/`format.ts` stay unchanged and none
|
|
29
|
+
* the wiser) by substituting an unambiguous `[redacted: N bytes]`
|
|
30
|
+
* placeholder wherever real content used to be — so a replayed
|
|
31
|
+
* `tasks`/`status`/`tasks --follow` view degrades to showing sizes
|
|
32
|
+
* instead of content, which is the correct, honest trade-off for a
|
|
33
|
+
* broadly-readable-by-default durable file. The LIVE human-facing stdout
|
|
34
|
+
* stream (`bin/commands/start.ts`, fed directly from the in-memory
|
|
35
|
+
* `DaemonEvent` before it ever reaches this module) is unaffected and
|
|
36
|
+
* keeps full fidelity — this redaction applies ONLY to what's written to
|
|
37
|
+
* disk.
|
|
38
|
+
* 2. **Permissions**: the file is tightened to 0600 BEFORE any new content
|
|
39
|
+
* is written, on every single append (mirrors `util/atomic-write.ts`'s
|
|
40
|
+
* own defensive re-chmod, for the identical reason: `open()`'s own
|
|
41
|
+
* `mode` argument only governs permissions at CREATION time, so a
|
|
42
|
+
* pre-existing file — predating this fix, or loosened by something else
|
|
43
|
+
* — keeps whatever mode it already had until explicitly chmod'd).
|
|
44
|
+
* `storeDir` gets the same defensive re-`chmod` to 0700 on every append.
|
|
45
|
+
*
|
|
46
|
+
* Finding P1 #3b (STILL-OPEN, now fixed): the chmod used to run AFTER
|
|
47
|
+
* `appendFile` rather than before — for a pre-existing permissive file
|
|
48
|
+
* that meant the newly-appended line briefly landed in a file that was
|
|
49
|
+
* still world-readable, and if `appendFile` itself failed, the chmod
|
|
50
|
+
* (being sequenced after it) never ran at all, leaving the file exactly
|
|
51
|
+
* as permissive as it started. Fixed by reordering so the chmod always
|
|
52
|
+
* happens FIRST: there is no window where a new append lands in a
|
|
53
|
+
* still-permissive file, and the tightening takes effect even when the
|
|
54
|
+
* append that follows it fails. This closes the gap going forward; it
|
|
55
|
+
* does NOT retroactively scrub plaintext a pre-fix build of this code
|
|
56
|
+
* may have already written into an inherited permissive file — only
|
|
57
|
+
* that the file stops being world-readable from the first append
|
|
58
|
+
* onward.
|
|
59
|
+
*
|
|
60
|
+
* Finding P2/#11 (audit half): also bounded/rotated now — `appendAuditEvent`
|
|
61
|
+
* checks the file's size (cheap `fs.stat`) on every append and, once it
|
|
62
|
+
* exceeds `MAX_AUDIT_LOG_BYTES`, atomically rewrites it down to the most
|
|
63
|
+
* recent `AUDIT_LOG_TRIM_TARGET_LINES` lines (reusing `atomicWriteFile`, so
|
|
64
|
+
* a concurrent `tasks`/`tasks --follow` read never observes a torn file
|
|
65
|
+
* mid-rotation) — so a long-lived daemon's audit.jsonl no longer grows
|
|
66
|
+
* forever. `followAuditLog` no longer re-reads the entire file every poll
|
|
67
|
+
* either; see its own doc comment.
|
|
68
|
+
*
|
|
69
|
+
* Finding P2 (new, round 1, now fixed): that trim used to be purely
|
|
70
|
+
* positional (the most recent N lines, full stop) — which could evict
|
|
71
|
+
* EVERY event a still-running task ever had if it went quiet for long
|
|
72
|
+
* enough, permanently erasing it from `tasks`/`status` even though it was
|
|
73
|
+
* never actually done. `compactPreservingLiveTasks` now preserves one
|
|
74
|
+
* lifecycle-anchor line for any still-open (non-terminal) task that would
|
|
75
|
+
* otherwise be fully evicted — see its own doc comment.
|
|
76
|
+
*
|
|
77
|
+
* Finding P2 (new, round 2, now fixed): round 1's anchor preservation had no
|
|
78
|
+
* cap of its own — a daemon that crashes leaving many non-terminal tasks
|
|
79
|
+
* behind (or one that keeps creating non-terminal tasks that never reach a
|
|
80
|
+
* terminal kind) accumulates one anchor per distinct non-terminal taskId
|
|
81
|
+
* FOREVER, defeating `MAX_AUDIT_LOG_BYTES`'s own size cap (the whole point
|
|
82
|
+
* of rotation) and making every append past the cap an ever-larger O(n)
|
|
83
|
+
* read + atomic-rewrite. `compactPreservingLiveTasks` now also bounds the
|
|
84
|
+
* anchors themselves to `MAX_LIVE_TASK_ANCHORS`, keeping the most
|
|
85
|
+
* recently-touched ones and dropping the oldest overflow with a single
|
|
86
|
+
* logged warning — see that constant's own doc comment.
|
|
87
|
+
*/
|
|
88
|
+
export declare function auditLogPath(storeDir: string): string;
|
|
89
|
+
/**
|
|
90
|
+
* Finding P2/#11 (audit half): rotate once the file exceeds this size,
|
|
91
|
+
* trimming to the most recent `AUDIT_LOG_TRIM_TARGET_LINES` lines. Checked
|
|
92
|
+
* via a cheap `fs.stat` (O(1), not a full read) on every single append; the
|
|
93
|
+
* expensive read-all-lines + atomic-rewrite only actually runs once the cap
|
|
94
|
+
* is crossed, and trimming well under the cap (redacted lines are small —
|
|
95
|
+
* mostly ids/sizes/counts) means it won't immediately re-trigger on the next
|
|
96
|
+
* append either. Deliberately simple size/line based bounding, not a
|
|
97
|
+
* time-based retention policy — "keep it simple" per this finding's own
|
|
98
|
+
* scope note.
|
|
99
|
+
*/
|
|
100
|
+
export declare const MAX_AUDIT_LOG_BYTES: number;
|
|
101
|
+
export declare const AUDIT_LOG_TRIM_TARGET_LINES = 5000;
|
|
102
|
+
/**
|
|
103
|
+
* Finding P2 (new, round 2): the hard cap on how many non-terminal-task
|
|
104
|
+
* lifecycle anchors {@link compactPreservingLiveTasks} will ever preserve in
|
|
105
|
+
* a single rotation — without this, a daemon that crashes leaving many
|
|
106
|
+
* non-terminal tasks behind (or a bug that keeps creating tasks that never
|
|
107
|
+
* reach a terminal kind) accumulates one anchor per distinct taskId with no
|
|
108
|
+
* upper bound, defeating `MAX_AUDIT_LOG_BYTES`'s own size cap and turning
|
|
109
|
+
* every subsequent append into an ever-larger O(n) read + atomic-rewrite.
|
|
110
|
+
* Mirrors `daemon/observer.ts`'s `MAX_TRACKED_TASKS`/`task-runner.ts`'s
|
|
111
|
+
* `MAX_TRACKED_TASK_IDS` registries: bounded to a generously-sized "a
|
|
112
|
+
* handful of genuinely concurrent tasks never gets close to this" cap, kept
|
|
113
|
+
* the most RECENTLY-touched (by last-seen line index) and dropping the
|
|
114
|
+
* oldest overflow with a single `console.warn` per rotation (not one per
|
|
115
|
+
* dropped anchor — a runaway leak should be visible without spamming the
|
|
116
|
+
* log). A dropped task simply reverts to the pre-round-1 behavior (falls
|
|
117
|
+
* out of `tasks`/`status` once its own events age out of the retained
|
|
118
|
+
* tail) rather than the file growing without bound.
|
|
119
|
+
*/
|
|
120
|
+
export declare const MAX_LIVE_TASK_ANCHORS = 500;
|
|
121
|
+
/**
|
|
122
|
+
* Appends one `DaemonEvent` as a single redacted JSON line (finding P1 #3 —
|
|
123
|
+
* see this file's module doc comment) at 0600, and rotates the file if it's
|
|
124
|
+
* grown past the size cap (finding P2/#11). Plain `fs.open('a')` +
|
|
125
|
+
* `appendFile` — sufficient for an append-only log (no torn-read risk the
|
|
126
|
+
* way a whole-file replace has; see `util/atomic-write.ts`'s own doc comment
|
|
127
|
+
* for why THAT helper exists for whole-file writes instead of just being
|
|
128
|
+
* used here too — rotation below is the one path in this file that DOES
|
|
129
|
+
* replace the whole file, and does use it).
|
|
130
|
+
*/
|
|
131
|
+
export declare function appendAuditEvent(storeDir: string, event: DaemonEvent): Promise<void>;
|
|
132
|
+
/**
|
|
133
|
+
* Adapts {@link appendAuditEvent} into a `DaemonEventListener` for
|
|
134
|
+
* `daemon.subscribe()` (used by the `start` command). `DaemonEventListener`
|
|
135
|
+
* is synchronous — `observer.ts`'s `emit()` never awaits a listener (see its
|
|
136
|
+
* own doc comment on why, including the finding #6 async-safety fix) — so a
|
|
137
|
+
* naive fire-and-forget `appendAuditEvent(...).catch(...)` per call would
|
|
138
|
+
* have a real problem: the underlying `fs.open`/`appendFile` calls could
|
|
139
|
+
* complete OUT OF ORDER under load, silently corrupting the log's own event
|
|
140
|
+
* ordering. This chains each append onto the previous one (still
|
|
141
|
+
* async/non-blocking from the listener's point of view) so writes always
|
|
142
|
+
* land in emission order, and a failed write (reported via `onError`) never
|
|
143
|
+
* breaks the chain for subsequent events.
|
|
144
|
+
*/
|
|
145
|
+
export declare function createAuditAppender(storeDir: string, onError?: (err: unknown) => void): DaemonEventListener;
|
|
146
|
+
/** Full historical read (oldest first) — `[]` if the log doesn't exist yet (e.g. `start` has never run). Used by `status`/`tasks` (no `--follow`). */
|
|
147
|
+
export declare function readAuditEvents(storeDir: string): Promise<DaemonEvent[]>;
|
|
148
|
+
export interface FollowOptions {
|
|
149
|
+
signal: AbortSignal;
|
|
150
|
+
/** How often to check the file for new bytes. Default 200ms — plenty responsive for a human tailing output, without busy-looping. */
|
|
151
|
+
pollIntervalMs?: number;
|
|
152
|
+
/**
|
|
153
|
+
* `true` (default): start at the file's CURRENT length — a `tail -f`,
|
|
154
|
+
* streaming only events appended from the moment `followAuditLog` was
|
|
155
|
+
* called. `false`: replay the full existing log first, then keep
|
|
156
|
+
* streaming — mainly useful for tests that want both in one pass.
|
|
157
|
+
*/
|
|
158
|
+
fromEnd?: boolean;
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Tails `filePath` (the audit log) for newly-appended `DaemonEvent` lines
|
|
162
|
+
* until `options.signal` aborts. No native filesystem-event watching (no
|
|
163
|
+
* `fs.watch` — inconsistent across platforms, especially the rename-based
|
|
164
|
+
* atomic writes this package uses elsewhere for OTHER files).
|
|
165
|
+
*
|
|
166
|
+
* Finding P2/#11 (audit half): the previous implementation re-read the
|
|
167
|
+
* ENTIRE file from byte 0 on every single poll, re-parsing every
|
|
168
|
+
* already-emitted line just to find the handful of new ones at the end —
|
|
169
|
+
* O(file size) work every `pollIntervalMs`, forever, for as long as
|
|
170
|
+
* `tasks --follow` stays attached. This tracks a byte OFFSET across polls
|
|
171
|
+
* instead: each poll opens the file and reads only the bytes appended since
|
|
172
|
+
* the last read (`FileHandle.read` at a specific position), advancing
|
|
173
|
+
* `offset` by exactly that many bytes — only genuinely new bytes are ever
|
|
174
|
+
* read or decoded.
|
|
175
|
+
*
|
|
176
|
+
* UTF-8 safety: the new bytes are split on the RAW BYTE `0x0a` ('\n')
|
|
177
|
+
* BEFORE any UTF-8 decoding happens, not on an already-decoded string —
|
|
178
|
+
* `0x0A` can only ever appear as an actual newline in valid UTF-8
|
|
179
|
+
* (continuation bytes are always `0x80`-`0xBF`), so this never risks
|
|
180
|
+
* decoding a torn multi-byte character that happened to straddle a chunk
|
|
181
|
+
* boundary. Bytes after the last `\n` in a chunk are held in `pending`
|
|
182
|
+
* (as a `Buffer`, still undecoded) until a future poll's bytes complete
|
|
183
|
+
* that line — mirrors `completeLines`'s existing "never guess at a torn
|
|
184
|
+
* trailing line" contract for `readAuditEvents`, just incremental.
|
|
185
|
+
*
|
|
186
|
+
* Rotation tolerance: if `appendAuditEvent`'s own size-cap rotation (or
|
|
187
|
+
* anything else) replaces the file out from under an in-progress follow,
|
|
188
|
+
* `offset` resets to 0 and `pending` is cleared — the follow session
|
|
189
|
+
* survives (rather than throwing or garbling) at the cost of re-emitting
|
|
190
|
+
* the post-rotation file's retained lines as if they were new.
|
|
191
|
+
*
|
|
192
|
+
* Finding P2 (new): that replacement is detected by file IDENTITY
|
|
193
|
+
* (`dev:ino`), not by comparing sizes. The previous implementation only
|
|
194
|
+
* reset `offset` when the new file was SMALLER than it (`st.size <
|
|
195
|
+
* offset`) — sufficient for the common case (a big pre-rotation file
|
|
196
|
+
* trimmed down to a small one), but `rotateIfNeeded`'s replace is an atomic
|
|
197
|
+
* temp-file + `fs.rename` (see `atomicWriteFile`), which swaps the
|
|
198
|
+
* underlying INODE regardless of whether the new file happens to be
|
|
199
|
+
* smaller OR LARGER than the reader's current offset. A `--follow` session
|
|
200
|
+
* that attached while the log was still tiny (a small `offset`) can have a
|
|
201
|
+
* rotation land a compacted-but-still-much-bigger-than-that-tiny-offset
|
|
202
|
+
* file in its place — the old size-only check missed exactly that case
|
|
203
|
+
* (`st.size < offset` was false), so it kept reading from the stale
|
|
204
|
+
* `offset` INTO THE NEW FILE's bytes: skipping that file's own prefix
|
|
205
|
+
* entirely and very likely starting mid-JSON-line. Tracking `dev:ino`
|
|
206
|
+
* across polls catches ANY replacement — smaller, larger, anything — and
|
|
207
|
+
* resets `offset` to 0 so the new file is always re-read from its own
|
|
208
|
+
* start. The size-based check is kept as a secondary fallback (a
|
|
209
|
+
* same-identity in-place truncate, or a platform where `ino` isn't a
|
|
210
|
+
* reliable identity signal).
|
|
211
|
+
*/
|
|
212
|
+
export declare function followAuditLog(filePath: string, onEvent: (event: DaemonEvent) => void, options: FollowOptions): Promise<void>;
|