@byok-sdk/client 0.2.0 → 0.4.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/README.md +58 -5
- package/dist/adapters/claude/claude-adapter.d.ts +6 -19
- package/dist/adapters/claude/events.d.ts +3 -0
- package/dist/adapters/claude/process-client.d.ts +9 -1
- package/dist/adapters/codex/codex-adapter.d.ts +4 -15
- package/dist/adapters/codex/process-runner.d.ts +4 -1
- package/dist/adapters/index.d.ts +4 -2
- package/dist/adapters/index.js +1081 -258
- package/dist/adapters/index.js.map +1 -1
- package/dist/adapters/pi/pi-adapter.d.ts +24 -15
- package/dist/adapters/pi/rpc-client.d.ts +9 -1
- package/dist/adapters/process-tree.d.ts +19 -0
- package/dist/adapters/provider-credential-environment.d.ts +18 -0
- package/dist/bin/audit-log.d.ts +12 -0
- package/dist/bin/byok-agent.js +2686 -912
- package/dist/bin/byok-agent.js.map +1 -1
- package/dist/bin/byok-approval-mcp.js +2 -2
- package/dist/bin/byok-approval-mcp.js.map +1 -1
- package/dist/bin/commands/workspaces.d.ts +11 -0
- package/dist/bin/format.d.ts +13 -0
- package/dist/bin/runtime-probe.d.ts +1 -1
- package/dist/bin/tasks-view.d.ts +13 -0
- package/dist/daemon/approvals.d.ts +2 -2
- package/dist/daemon/assertion-client.d.ts +68 -0
- package/dist/daemon/capabilities-client.d.ts +48 -0
- package/dist/daemon/connection-manager.d.ts +4 -2
- package/dist/daemon/control-protocol.d.ts +81 -4
- package/dist/daemon/control-server.d.ts +18 -1
- package/dist/daemon/create-daemon.d.ts +171 -3
- package/dist/daemon/daemon-owner.d.ts +37 -0
- package/dist/daemon/device-assertion-signer.d.ts +41 -0
- package/dist/daemon/device-keys.d.ts +15 -13
- package/dist/daemon/environment.d.ts +9 -9
- package/dist/daemon/git-workspace.d.ts +21 -0
- package/dist/daemon/observer.d.ts +81 -3
- package/dist/daemon/presence-publisher.d.ts +98 -0
- package/dist/daemon/runtime-capabilities.d.ts +1 -1
- package/dist/daemon/skill-pack-installer.d.ts +116 -0
- package/dist/daemon/task-runner.d.ts +156 -37
- package/dist/daemon/ws-transport.d.ts +3 -1
- package/dist/index.d.ts +25 -4
- package/dist/index.js +2972 -597
- package/dist/index.js.map +1 -1
- package/dist/runtime-failure.d.ts +64 -0
- package/dist/types.d.ts +114 -58
- package/package.json +4 -4
|
@@ -27,12 +27,20 @@ export declare const HANDSHAKE_TIMEOUT_MS = 3000;
|
|
|
27
27
|
* time this matters — see `control-server.ts`'s `startControlServer`);
|
|
28
28
|
* falls back, whenever the natural path would risk exceeding {@link
|
|
29
29
|
* UNIX_SOCKET_PATH_SOFT_LIMIT}, to a short, deterministic path nested under
|
|
30
|
-
* a PER-DAEMON PRIVATE subdirectory of
|
|
31
|
-
* of `storeDir` alone, so both the daemon and any CLI
|
|
32
|
-
* the same `storeDir` independently compute the
|
|
30
|
+
* a PER-DAEMON PRIVATE subdirectory of {@link CONTROL_SOCKET_FALLBACK_ROOT}
|
|
31
|
+
* — derived from a hash of `storeDir` alone, so both the daemon and any CLI
|
|
32
|
+
* invocation pointed at the same `storeDir` independently compute the
|
|
33
|
+
* identical fallback path.
|
|
34
|
+
*
|
|
35
|
+
* That root was `os.tmpdir()` until it was proven to break both halves of
|
|
36
|
+
* that sentence: it reads `TMPDIR`, so the daemon (under a service manager)
|
|
37
|
+
* and the CLI (in an operator shell) derived DIFFERENT addresses for one
|
|
38
|
+
* store, and under a `TMPDIR` nested in the same long tree the fallback came
|
|
39
|
+
* out LONGER than the path it escaped — `bind()` `EINVAL`, and the daemon
|
|
40
|
+
* ran on with no control socket at all.
|
|
33
41
|
*
|
|
34
42
|
* Nested one level deep (rather than a bare `<hash>.sock` file directly in
|
|
35
|
-
*
|
|
43
|
+
* that shared, world-traversable root) specifically so
|
|
36
44
|
* `control-server.ts`'s `bindControlEndpoint` can create+chmod that
|
|
37
45
|
* subdirectory 0700 BEFORE ever binding inside it — the directory's own
|
|
38
46
|
* mode gates traversal into it regardless of the socket file's own
|
|
@@ -292,6 +300,75 @@ export interface ApprovalsRequestResult {
|
|
|
292
300
|
approved: boolean;
|
|
293
301
|
reason?: string;
|
|
294
302
|
}
|
|
303
|
+
/**
|
|
304
|
+
* Params for `assertion.issue`: a sibling local process (the host's own CLI,
|
|
305
|
+
* installed alongside this daemon) asking the daemon to mint one short-lived,
|
|
306
|
+
* audience-scoped device assertion with the paired device key. See
|
|
307
|
+
* `@byok-sdk/core`'s `device-assertion.ts` for the envelope, and
|
|
308
|
+
* `create-daemon.ts`'s handler for the six fail-closed gates every call passes
|
|
309
|
+
* through in a fixed order.
|
|
310
|
+
*
|
|
311
|
+
* One field, and nothing else. In particular there is deliberately no caller
|
|
312
|
+
* identity, no requested TTL, and no requested claim set: every process running
|
|
313
|
+
* as this UID can reach the control socket, so anything a caller "tells" the
|
|
314
|
+
* daemon about itself is decoration, and a caller-chosen lifetime is just the
|
|
315
|
+
* TTL ceiling handed to whoever asks.
|
|
316
|
+
*/
|
|
317
|
+
export interface AssertionIssueParams {
|
|
318
|
+
audience: string;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Bound on the `audience` a caller may send, in UTF-8 bytes — mirrors
|
|
322
|
+
* `@byok-sdk/core`'s `DEVICE_ASSERTION_AUDIENCE_MAX_BYTES`. Restated here rather
|
|
323
|
+
* than imported so the WIRE bound is checked before anything reaches the claim
|
|
324
|
+
* schema: this is the frame-level shape gate, and it must reject an oversized
|
|
325
|
+
* value without that value ever reaching a signer or an audit line.
|
|
326
|
+
*/
|
|
327
|
+
export declare const ASSERTION_AUDIENCE_MAX_BYTES = 256;
|
|
328
|
+
/**
|
|
329
|
+
* Strict shape check. `undefined` means `bad_request` — a distinct gate from
|
|
330
|
+
* `audience_denied` (see `create-daemon.ts`): "you sent something that is not a
|
|
331
|
+
* request" and "you asked for an audience you may not have" are different
|
|
332
|
+
* facts, and collapsing them would let a caller probe the allowlist by
|
|
333
|
+
* malforming requests.
|
|
334
|
+
*
|
|
335
|
+
* Rejects an unknown key outright rather than ignoring it. A tolerated extra
|
|
336
|
+
* field is how a future caller comes to believe it can influence the claim set.
|
|
337
|
+
*/
|
|
338
|
+
export declare function parseAssertionIssueParams(value: unknown): AssertionIssueParams | undefined;
|
|
339
|
+
/**
|
|
340
|
+
* Result of `assertion.issue`. `assertion` is the full signing envelope
|
|
341
|
+
* (`DeviceAssertionEnvelopeV1`), carried as an opaque JSON value on this wire —
|
|
342
|
+
* the caller hands it to the host's cloud, which parses and verifies it with
|
|
343
|
+
* core's own `verifyDeviceAssertion`. `expiresAt` is repeated outside the
|
|
344
|
+
* envelope purely so a caller can schedule a refresh without parsing claims it
|
|
345
|
+
* has no business interpreting.
|
|
346
|
+
*/
|
|
347
|
+
export interface AssertionIssueResult {
|
|
348
|
+
assertion: unknown;
|
|
349
|
+
expiresAt: string;
|
|
350
|
+
}
|
|
351
|
+
/**
|
|
352
|
+
* The six `ControlError` codes `assertion.issue` can answer with, in the exact
|
|
353
|
+
* order the handler checks them (`create-daemon.ts`). Each one is a distinct
|
|
354
|
+
* refusal with a distinct cause; none of them ever signs anything.
|
|
355
|
+
*
|
|
356
|
+
* - `assertion_disabled` — this daemon has no `deviceAssertion` config, or an
|
|
357
|
+
* empty audience allowlist. The feature is OFF by default.
|
|
358
|
+
* - `bad_request` — params were not `{audience: string}` within the byte bound.
|
|
359
|
+
* - `audience_denied` — the audience is not in the configured allowlist. The
|
|
360
|
+
* message never echoes the allowlist: a refusal must not be an enumeration
|
|
361
|
+
* oracle.
|
|
362
|
+
* - `shutting_down` — a shutdown has been requested. Closes the window between
|
|
363
|
+
* the shutdown RPC being acknowledged and the control socket actually
|
|
364
|
+
* closing, during which a device that is being unpaired could otherwise still
|
|
365
|
+
* mint credentials.
|
|
366
|
+
* - `revoked` — the server has revoked this device.
|
|
367
|
+
* - `not_paired` — there is no device record on disk (never paired, or already
|
|
368
|
+
* cleared).
|
|
369
|
+
*/
|
|
370
|
+
export declare const ASSERTION_ISSUE_ERROR_CODES: readonly ['assertion_disabled', 'bad_request', 'audience_denied', 'shutting_down', 'revoked', 'not_paired'];
|
|
371
|
+
export type AssertionIssueErrorCode = (typeof ASSERTION_ISSUE_ERROR_CODES)[number];
|
|
295
372
|
export type ShutdownReason = 'unpair' | 'operator';
|
|
296
373
|
export interface ShutdownParams {
|
|
297
374
|
reason?: ShutdownReason;
|
|
@@ -38,7 +38,24 @@ export interface ControlServerOptions {
|
|
|
38
38
|
export interface ControlServerHandle {
|
|
39
39
|
/** The bound Unix socket path or Windows pipe name. */
|
|
40
40
|
endpoint: string;
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Stops accepting new connections and destroys every open one, WITHOUT
|
|
43
|
+
* removing the socket/token files. Idempotent, and the first half of
|
|
44
|
+
* {@link close}.
|
|
45
|
+
*
|
|
46
|
+
* Split out from `close()` for `create-daemon.ts`'s shutdown sequence
|
|
47
|
+
* (plan `shutdown-lease-order`): the socket/token files ARE this daemon's
|
|
48
|
+
* only external liveness signal (`isControlDaemonGone` in
|
|
49
|
+
* `bin/control-client.ts` reads "token file gone AND a connect refused"),
|
|
50
|
+
* so a shutdown must be able to stop serving RPCs, release its
|
|
51
|
+
* store-mutation lease, and only THEN publish "gone" by removing the
|
|
52
|
+
* files. Between the two stages a connect is already refused while the
|
|
53
|
+
* token file still exists — which reads as "still running", the
|
|
54
|
+
* conservative answer, rather than as "exited while still holding the
|
|
55
|
+
* lease".
|
|
56
|
+
*/
|
|
57
|
+
stopServing(): Promise<void>;
|
|
58
|
+
/** Runs {@link stopServing}, then removes the socket/token files (a Windows pipe leaves no file to remove). Idempotent. */
|
|
42
59
|
close(): Promise<void>;
|
|
43
60
|
}
|
|
44
61
|
/**
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { RuntimeId } from '@byok-sdk/protocol';
|
|
2
2
|
import type { PermissionPolicy } from '@byok-sdk/protocol';
|
|
3
|
-
import type { RuntimeAdapter, GitWorkspaceConfig } from '../types';
|
|
3
|
+
import type { RuntimeAdapter, GitWorkspaceConfig, McpToolsetConfig } from '../types';
|
|
4
4
|
import type { BackoffOptions, LivenessOptions } from './ws-transport';
|
|
5
5
|
import { type OperationalHealthSnapshot } from './operational-health';
|
|
6
6
|
import { type DaemonEventListener, type DaemonTaskInfo, type Unsubscribe } from './observer';
|
|
@@ -10,6 +10,7 @@ import { type DeviceRecord } from './store';
|
|
|
10
10
|
import { type LocalTaskJournal } from './journal/journal';
|
|
11
11
|
import { type JournalOpenFaultSeam } from './journal/sqlite-support';
|
|
12
12
|
import { LocalStoragePressureEngine, type LocalStoragePolicyInput } from './journal/storage-policy';
|
|
13
|
+
import { type ResultDocumentExtractor } from './task-runner';
|
|
13
14
|
import type { ProgressBatcherOptions } from './progress-batcher';
|
|
14
15
|
/**
|
|
15
16
|
* Optional white-label product display info — purely opaque passthrough
|
|
@@ -104,12 +105,18 @@ export interface DaemonConfig {
|
|
|
104
105
|
* unchanged from M1/M2.
|
|
105
106
|
*/
|
|
106
107
|
runtimeAllowlist?: string[];
|
|
108
|
+
/**
|
|
109
|
+
* Separate-process Pi BYOK credential boundary. Required only for a
|
|
110
|
+
* `dispatchSelection` in the BYOK lane; subscription runtimes and legacy
|
|
111
|
+
* Pi tasks do not invoke it.
|
|
112
|
+
*/
|
|
113
|
+
piByokLauncher?: import('../adapters/pi/pi-adapter').PiByokLauncherConfig;
|
|
107
114
|
/**
|
|
108
115
|
* M5 batch-3 (workstream 1): explicit auto-select priority order for
|
|
109
116
|
* `TaskRunner.pickAdapter`'s no-explicit-runtime branch (`task-runner.ts`)
|
|
110
117
|
* — tried in listed order; the first candidate that is both PRESENT
|
|
111
118
|
* (`adapter.detect()`) and CAPABLE (declares the offer's
|
|
112
|
-
* `PermissionPolicy.mode` in its own `capabilities
|
|
119
|
+
* `PermissionPolicy.mode` in its own `descriptor.capabilities.permissionModes` —
|
|
113
120
|
* see `adapterSupportsMode`) wins. Unset defaults to
|
|
114
121
|
* `DEFAULT_RUNTIME_PREFERENCE` (`task-runner.ts`): `['claude', 'codex',
|
|
115
122
|
* 'pi']` — pi LAST, deliberately.
|
|
@@ -169,12 +176,19 @@ export interface DaemonConfig {
|
|
|
169
176
|
* an id with no matching adapter is simply never looked up). `allow`
|
|
170
177
|
* entries are exact variable names or `*`-suffixed prefixes, merged in
|
|
171
178
|
* alongside that runtime adapter's own declared
|
|
172
|
-
* `environmentRequirements
|
|
179
|
+
* `descriptor.environmentRequirements` — this can never override the hard
|
|
173
180
|
* `BYOK_*` deny (see `environment.ts`'s own doc comment).
|
|
174
181
|
*/
|
|
175
182
|
runtimeEnvironment?: Record<string, {
|
|
176
183
|
allow?: string[];
|
|
177
184
|
}>;
|
|
185
|
+
/**
|
|
186
|
+
* Device-local registry behind wire-level `requiredToolsets` ids. Only
|
|
187
|
+
* logical ids cross the SaaS wire; MCP executable definitions stay here.
|
|
188
|
+
* The first slice supports stdio servers (`command` + `args`) only and
|
|
189
|
+
* deliberately has no task-provided env/header/secret surface.
|
|
190
|
+
*/
|
|
191
|
+
mcpToolsets?: Record<string, McpToolsetConfig>;
|
|
178
192
|
/**
|
|
179
193
|
* M5: explicit escape hatch for `url.ts`'s `assertServerUrlAllowed` — see
|
|
180
194
|
* that function's own doc comment for the full allow/deny rule. Default
|
|
@@ -209,6 +223,48 @@ export interface DaemonConfig {
|
|
|
209
223
|
* explicitly instead to opt out of enforcement altogether.
|
|
210
224
|
*/
|
|
211
225
|
maxTaskOutputBytes?: number;
|
|
226
|
+
/**
|
|
227
|
+
* additive-minor (`task.complete.document`): the seam through which this
|
|
228
|
+
* product turns a finished task's final output text into the STRUCTURED
|
|
229
|
+
* terminal result the wire carries as `task.complete.document`, and the
|
|
230
|
+
* server projects into `TaskResult.document`.
|
|
231
|
+
*
|
|
232
|
+
* `extract(finalOutput, task)` is called exactly once per task, at the
|
|
233
|
+
* moment `task.complete` is built, with the same text that becomes
|
|
234
|
+
* `summary` (the concatenated `progress` events for that task) plus the
|
|
235
|
+
* task's `taskId`/`sessionRef`. Return `undefined` for "no structured
|
|
236
|
+
* result this time". Everything about the document's SHAPE is the
|
|
237
|
+
* product's business — the SDK never inspects, validates, or transforms
|
|
238
|
+
* it; extraction logic (prompting for JSON, parsing a fenced block,
|
|
239
|
+
* validating against the product's own schema) is product glue and belongs
|
|
240
|
+
* in this callback, not in the SDK.
|
|
241
|
+
*
|
|
242
|
+
* The SDK enforces exactly two wire rules, via the protocol's own
|
|
243
|
+
* `checkResultDocument`: the value must be JSON-serializable, and at most
|
|
244
|
+
* `RESULT_DOCUMENT_MAX_BYTES` (1 MiB) as canonical JSON. Stay under ~512
|
|
245
|
+
* KiB in practice (docs/protocol.md); a bigger result belongs in an
|
|
246
|
+
* artifact, not here.
|
|
247
|
+
*
|
|
248
|
+
* FAIL-CLOSED, never silent: if the extractor throws, returns a promise
|
|
249
|
+
* (the seam is synchronous and the runtime enforces it — an unawaited
|
|
250
|
+
* promise would be encoded as an empty document), produces something
|
|
251
|
+
* unsendable, or produces a document while the connected server never
|
|
252
|
+
* advertised the `result-document` capability (an old server would strip
|
|
253
|
+
* the field on arrival without a word), the task is reported as
|
|
254
|
+
* `task.fail` with `retryable: false` and a reason prefixed
|
|
255
|
+
* `result document undeliverable` — see
|
|
256
|
+
* `RESULT_DOCUMENT_UNDELIVERABLE_REASON_PREFIX` (`task-runner.ts`).
|
|
257
|
+
* Completing a task while quietly discarding the structured result it
|
|
258
|
+
* exists to produce is not an option this SDK offers.
|
|
259
|
+
*
|
|
260
|
+
* Omitted entirely by default, in which case the completion path is
|
|
261
|
+
* unchanged in every respect — no extractor call, no capability check, and
|
|
262
|
+
* a `task.complete` payload byte-identical to the one sent before this
|
|
263
|
+
* field existed.
|
|
264
|
+
*/
|
|
265
|
+
resultDocument?: {
|
|
266
|
+
readonly extract: ResultDocumentExtractor;
|
|
267
|
+
};
|
|
212
268
|
/**
|
|
213
269
|
* M5 batch-3 (workstream 2): deadline bound on the graceful-shutdown
|
|
214
270
|
* sequence's own wait for `TaskRunner.shutdownActiveTasks` to finish
|
|
@@ -221,6 +277,85 @@ export interface DaemonConfig {
|
|
|
221
277
|
* existed.
|
|
222
278
|
*/
|
|
223
279
|
shutdownGraceMs?: number;
|
|
280
|
+
/**
|
|
281
|
+
* Cadence for the `online` presence heartbeat (§12.3), when — and only when
|
|
282
|
+
* — the deployment's capability declaration contains `presence.hints` (see
|
|
283
|
+
* `capabilities-client.ts`; a deployment that declares nothing, or one this
|
|
284
|
+
* daemon could not read a declaration from, publishes nothing at all).
|
|
285
|
+
*
|
|
286
|
+
* Every field is optional and defaults to `presence-publisher.ts`'s own
|
|
287
|
+
* constants, which are chosen against the hosted defaults. `ttlMs` and
|
|
288
|
+
* `minimumIntervalMs` describe THE DEPLOYMENT's hint TTL and publication
|
|
289
|
+
* throttle as this operator understands them: the daemon never learns either
|
|
290
|
+
* from the wire, and uses them only to validate `intervalMs` sits strictly
|
|
291
|
+
* between them — a cadence outside that band is rejected synchronously here,
|
|
292
|
+
* the same way `maxTaskOutputBytes` is above, rather than degrading into a
|
|
293
|
+
* rate-limited or flickering hint nobody sees an error for.
|
|
294
|
+
*/
|
|
295
|
+
presence?: PresenceConfig;
|
|
296
|
+
/**
|
|
297
|
+
* Plan `device-assertion-broker`: opt-in local assertion broker — lets a
|
|
298
|
+
* sibling process on this same machine (typically the host's own CLI,
|
|
299
|
+
* installed alongside this daemon) ask the daemon, over the already
|
|
300
|
+
* authenticated control socket, to mint a short-lived audience-scoped
|
|
301
|
+
* assertion signed with the paired device key. See
|
|
302
|
+
* {@link DeviceAssertionConfig}.
|
|
303
|
+
*
|
|
304
|
+
* OFF by default, and off is expressed two ways that mean the same thing: an
|
|
305
|
+
* absent section, or a present one with an empty `audiences` list. Both make
|
|
306
|
+
* `assertion.issue` answer `assertion_disabled` without looking at anything
|
|
307
|
+
* else. A new local authentication surface does not get to be on because
|
|
308
|
+
* someone left a config key behind.
|
|
309
|
+
*/
|
|
310
|
+
deviceAssertion?: DeviceAssertionConfig;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Plan `device-assertion-broker`. Two fields, both about what this daemon will
|
|
314
|
+
* refuse.
|
|
315
|
+
*
|
|
316
|
+
* Every field is validated synchronously at construction, the same way
|
|
317
|
+
* `maxTaskOutputBytes` and the presence cadence are — a misconfigured
|
|
318
|
+
* authentication surface must fail when the daemon is built, not on the first
|
|
319
|
+
* call that needed it.
|
|
320
|
+
*/
|
|
321
|
+
export interface DeviceAssertionConfig {
|
|
322
|
+
/**
|
|
323
|
+
* The EXACT audience strings this daemon will mint for. Matched with
|
|
324
|
+
* `Set.has` — exact string equality, never a prefix or suffix or subdomain
|
|
325
|
+
* rule.
|
|
326
|
+
*
|
|
327
|
+
* Prefix matching is the classic hole here: an allowlist entry of
|
|
328
|
+
* `salesko-api` under a `startsWith` rule also admits `salesko-api.evil.com`,
|
|
329
|
+
* and a suffix rule admits `evil-salesko-api`. There is no configuration
|
|
330
|
+
* that turns this into a pattern match, because there is no pattern-matching
|
|
331
|
+
* code to configure.
|
|
332
|
+
*
|
|
333
|
+
* An empty list means the feature is off (see
|
|
334
|
+
* `DaemonConfig.deviceAssertion`). Duplicate entries, empty entries, and
|
|
335
|
+
* entries over 256 UTF-8 bytes are construction errors — a duplicate is
|
|
336
|
+
* usually a copy-paste that hid a typo'd second entry, and silently
|
|
337
|
+
* de-duplicating it would hide it for good.
|
|
338
|
+
*/
|
|
339
|
+
audiences: string[];
|
|
340
|
+
/**
|
|
341
|
+
* Assertion lifetime, ms. Default
|
|
342
|
+
* `DEVICE_ASSERTION_DEFAULT_TTL_MS` (120s), hard ceiling
|
|
343
|
+
* `DEVICE_ASSERTION_MAX_TTL_MS` (300s) — both from `@byok-sdk/core`, which
|
|
344
|
+
* enforces the same ceiling again at verification time, so a daemon patched
|
|
345
|
+
* to ignore this one still cannot get a longer-lived assertion accepted.
|
|
346
|
+
*
|
|
347
|
+
* Deliberately NOT caller-selectable over the control socket: a lifetime a
|
|
348
|
+
* caller can ask for is a lifetime every caller asks the maximum of.
|
|
349
|
+
*/
|
|
350
|
+
ttlMs?: number;
|
|
351
|
+
}
|
|
352
|
+
export interface PresenceConfig {
|
|
353
|
+
/** Heartbeat cadence. Default 30s. */
|
|
354
|
+
intervalMs?: number;
|
|
355
|
+
/** The deployment's presence hint TTL. Default 90s (core §12.7.5 suggests 60-120s). */
|
|
356
|
+
ttlMs?: number;
|
|
357
|
+
/** The deployment's minimum interval between accepted publications. Default 5s. */
|
|
358
|
+
minimumIntervalMs?: number;
|
|
224
359
|
}
|
|
225
360
|
export interface DaemonStatus {
|
|
226
361
|
paired: boolean;
|
|
@@ -340,7 +475,40 @@ export interface DaemonOverrides {
|
|
|
340
475
|
pressureEngine?: LocalStoragePressureEngine;
|
|
341
476
|
};
|
|
342
477
|
}
|
|
478
|
+
/**
|
|
479
|
+
* Plan `device-assertion-broker` (codex round-2 F3): the internal, NON-public
|
|
480
|
+
* test seam for observing assertion issuance.
|
|
481
|
+
*
|
|
482
|
+
* This is NOT reachable through `DaemonConfig`/`DaemonOverrides`, and is NOT
|
|
483
|
+
* re-exported from the package `index.ts` — a test imports it straight from
|
|
484
|
+
* this module. That isolation is the point. The earlier `DaemonOverrides.
|
|
485
|
+
* deviceAssertion.mint` seam replaced the SIGNER, which meant a production
|
|
486
|
+
* embedder (`DaemonOverrides` is public API) could inject a callback that
|
|
487
|
+
* received the whole `DeviceRecord` — private key included — and exfiltrate it
|
|
488
|
+
* or forge claims.
|
|
489
|
+
*
|
|
490
|
+
* `onIssued` is a strict OBSERVER, called only AFTER a real, successful sign,
|
|
491
|
+
* with non-secret metadata ONLY (`jti`, `audience`). It cannot see the private
|
|
492
|
+
* key, cannot alter the signature, the claims, or the audit event, and cannot
|
|
493
|
+
* be reached from any public type. A test counts these calls to prove a gate
|
|
494
|
+
* rejection never reached the signer (a rejection never calls `onIssued`).
|
|
495
|
+
*/
|
|
496
|
+
export interface AssertionIssueProbe {
|
|
497
|
+
onIssued(meta: {
|
|
498
|
+
jti: string;
|
|
499
|
+
audience: string;
|
|
500
|
+
}): void;
|
|
501
|
+
}
|
|
343
502
|
export declare function createDaemonWithAdapters(config: DaemonConfig, adapters: RuntimeAdapter[], overrides?: DaemonOverrides): Daemon;
|
|
503
|
+
/**
|
|
504
|
+
* codex round-2 F3: the real builder. Exported from THIS module but NOT from
|
|
505
|
+
* the package `index.ts`, so the optional `assertionProbe` (an
|
|
506
|
+
* {@link AssertionIssueProbe} post-sign observer) is reachable only by tests
|
|
507
|
+
* importing this module directly — never through any public type. The public
|
|
508
|
+
* `createDaemonWithAdapters` above forwards without it, so production has no
|
|
509
|
+
* observer and no signer-injection surface at all.
|
|
510
|
+
*/
|
|
511
|
+
export declare function buildDaemonWithAdapters(config: DaemonConfig, adapters: RuntimeAdapter[], overrides?: DaemonOverrides, assertionProbe?: AssertionIssueProbe): Daemon;
|
|
344
512
|
/**
|
|
345
513
|
* Public white-label entry point (M0-M3): the "5-line launcher" — a product
|
|
346
514
|
* only needs a `DaemonConfig`, no hand-built adapter list. The bundled
|
|
@@ -11,6 +11,43 @@ interface OwnerRecord {
|
|
|
11
11
|
export interface DaemonOwnerLease {
|
|
12
12
|
release(): Promise<void>;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Where this store's lock lives. Keyed by the CANONICAL storeDir alone — not
|
|
16
|
+
* by product id or OS user — because the store is the resource being
|
|
17
|
+
* serialized: a symlink alias and its target, or two products pointed at one
|
|
18
|
+
* store directory, must contend for the same endpoint (see
|
|
19
|
+
* `acquireDaemonOwner`'s own note on resolving aliases before deriving this).
|
|
20
|
+
*
|
|
21
|
+
* POSIX prefers `<storeDir>/mutex.sock`, keeping the lock beside the store
|
|
22
|
+
* state it guards in a directory `ensureSecureDir` has already made 0700. A
|
|
23
|
+
* storeDir can be long enough that no address under it fits
|
|
24
|
+
* {@link UNIX_SOCKET_PATH_SOFT_LIMIT} — a storeDir alone can exceed the whole
|
|
25
|
+
* `sun_path` budget — so the second candidate drops the storeDir from the path
|
|
26
|
+
* entirely and carries it as a hash under {@link STORE_MUTEX_FALLBACK_ROOT},
|
|
27
|
+
* nested one level deep so that directory can be created 0700 BEFORE anything
|
|
28
|
+
* binds inside it (the nesting convention is `controlSocketPath`'s).
|
|
29
|
+
*
|
|
30
|
+
* `controlSocketPath`'s fallback now binds under its own FIXED `/tmp` root
|
|
31
|
+
* (`control-protocol.ts`'s `CONTROL_SOCKET_FALLBACK_ROOT` — not
|
|
32
|
+
* `os.tmpdir()`); this mutex must not regress to `os.tmpdir()` for the same
|
|
33
|
+
* two independent reasons proven by CI job 94334133652. Correctness:
|
|
34
|
+
* `os.tmpdir()` is environment-derived, and a lock address that differs
|
|
35
|
+
* between two contending processes admits two writers. Reachability: the
|
|
36
|
+
* caller may have pointed `TMPDIR` INSIDE the very tree that made the natural
|
|
37
|
+
* path too long — there, `os.tmpdir()` yields an address LONGER than the one
|
|
38
|
+
* being escaped (152 bytes vs 126 in that job), so the "fallback" cannot bind
|
|
39
|
+
* at all. A fixed short root is immune to both.
|
|
40
|
+
*
|
|
41
|
+
* win32 has no socket file to place; a named pipe lives in one flat
|
|
42
|
+
* machine-wide namespace, so the name carries the store hash to keep two
|
|
43
|
+
* stores from colliding, and no length branch is needed.
|
|
44
|
+
*
|
|
45
|
+
* @internal Exported for the regression guard only (never re-exported from
|
|
46
|
+
* `index.ts`): the crashed-holder recovery path can only be exercised by
|
|
47
|
+
* planting a stale socket file at the exact address this derives, and a test
|
|
48
|
+
* recomputing the derivation itself would prove nothing about this one.
|
|
49
|
+
*/
|
|
50
|
+
export declare function storeMutexEndpoint(canonicalStoreDir: string, identity: string, platform?: NodeJS.Platform): string;
|
|
14
51
|
export declare class DaemonOwnerActiveError extends Error {
|
|
15
52
|
readonly role: OwnerRecord['role'] | 'unknown';
|
|
16
53
|
constructor(role: OwnerRecord['role'] | 'unknown');
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type DeviceAssertionClaims, type DeviceAssertionEnvelopeV1 } from '@byok-sdk/core';
|
|
2
|
+
import type { DeviceRecord } from './store';
|
|
3
|
+
/**
|
|
4
|
+
* Mints one device assertion (plan `device-assertion-broker`).
|
|
5
|
+
*
|
|
6
|
+
* This module exists so there is exactly ONE place in the client that touches
|
|
7
|
+
* the device private key for this envelope, and so "the key is never cached"
|
|
8
|
+
* is a property of a small readable file rather than a claim about a
|
|
9
|
+
* 2000-line one. Two rules it enforces structurally:
|
|
10
|
+
*
|
|
11
|
+
* 1. **No module-level key state.** There is no cache, no memo, no
|
|
12
|
+
* module-scope variable of any kind here. The `KeyObject` is created inside
|
|
13
|
+
* {@link mintDeviceAssertion} from the record the caller just read off disk
|
|
14
|
+
* and becomes unreachable when the function returns — the same
|
|
15
|
+
* read-the-store-every-time discipline `StoredDeviceProofSigner` documents,
|
|
16
|
+
* for the same reason: clearing `device.json` must remove local signing
|
|
17
|
+
* authority immediately, not at the next process restart.
|
|
18
|
+
* 2. **The caller supplies the record.** This function does not load the
|
|
19
|
+
* store, check revocation, or consult an allowlist. All of that is the
|
|
20
|
+
* daemon's fail-closed gate sequence (`create-daemon.ts`), and duplicating
|
|
21
|
+
* any of it here would create a second, quieter authority on whether an
|
|
22
|
+
* assertion may be minted at all.
|
|
23
|
+
*/
|
|
24
|
+
export interface MintDeviceAssertionInput {
|
|
25
|
+
/** The record just read from disk — never a cached one. */
|
|
26
|
+
readonly record: DeviceRecord;
|
|
27
|
+
/** The paired server's normalized origin (`url.ts`'s `toHttpBase` → `origin`). */
|
|
28
|
+
readonly issuer: string;
|
|
29
|
+
readonly productId: string;
|
|
30
|
+
/** Already checked against the configured allowlist by the caller. */
|
|
31
|
+
readonly audience: string;
|
|
32
|
+
/** Already range-checked at daemon construction time. */
|
|
33
|
+
readonly ttlMs: number;
|
|
34
|
+
readonly now: Date;
|
|
35
|
+
}
|
|
36
|
+
export interface MintedDeviceAssertion {
|
|
37
|
+
readonly envelope: DeviceAssertionEnvelopeV1;
|
|
38
|
+
readonly claims: DeviceAssertionClaims;
|
|
39
|
+
readonly expiresAt: string;
|
|
40
|
+
}
|
|
41
|
+
export declare function mintDeviceAssertion(input: MintDeviceAssertionInput): MintedDeviceAssertion;
|
|
@@ -24,22 +24,24 @@ export declare function exportPrivateKeyPem(privateKey: KeyObject): string;
|
|
|
24
24
|
export declare function importPrivateKeyPem(pem: string): KeyObject;
|
|
25
25
|
/**
|
|
26
26
|
* S1 (GAP-004): the domain-separation prefix this device signs along with a
|
|
27
|
-
* challenge nonce
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* those from being replayable as a token-renewal credential.
|
|
27
|
+
* challenge nonce. The device key is a long-lived identity key that later
|
|
28
|
+
* planes will also sign structured messages with; tagging the domain is what
|
|
29
|
+
* stops a signature made for one of those from being replayable as a
|
|
30
|
+
* token-renewal credential.
|
|
32
31
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
32
|
+
* The literal itself now lives in `@byok-sdk/core` (`src/pairing.ts`), which
|
|
33
|
+
* the daemon, the hosted surface, and the reference server all already depend
|
|
34
|
+
* on. It used to be three copies, each commented as byte-identical to the
|
|
35
|
+
* others — an agreement that holds only until someone edits one of them.
|
|
36
|
+
* Re-exported here so this module's public surface is unchanged.
|
|
36
37
|
*/
|
|
37
|
-
export
|
|
38
|
+
export { NONCE_SIGNING_DOMAIN } from '@byok-sdk/core';
|
|
38
39
|
/**
|
|
39
40
|
* Sign a challenge nonce with the device private key: the signed message is
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
41
|
+
* `NONCE_SIGNING_DOMAIN` followed by `nonce` (UTF-8) — core's
|
|
42
|
+
* {@link nonceSigningBytes} produces those bytes — and the result is the raw
|
|
43
|
+
* 64-byte Ed25519 signature, base64url-encoded (protocol §6.2). A server on the
|
|
44
|
+
* domain-separated contract rejects the undomained form, so there is no variant
|
|
45
|
+
* of this that omits the prefix.
|
|
44
46
|
*/
|
|
45
47
|
export declare function signNonce(privateKey: KeyObject, nonce: string): string;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* M5: per-runtime environment allowlist for spawned agent child processes.
|
|
3
3
|
*
|
|
4
4
|
* Before this module existed, `task-runner.ts` built every task's
|
|
5
|
-
* `
|
|
5
|
+
* `RuntimeOperationStartInput.env` as `process.env` verbatim — the daemon's OWN full
|
|
6
6
|
* environment, unfiltered, handed to whichever runtime CLI (`pi`/`claude`/
|
|
7
7
|
* `codex`) `pickAdapter` selected. Any credential-shaped variable sitting in
|
|
8
8
|
* the daemon's own environment for a completely unrelated reason (an
|
|
@@ -20,10 +20,9 @@
|
|
|
20
20
|
* behave sanely in a non-interactive shell.
|
|
21
21
|
* 2. Whatever ADDITIONAL names the *specific* runtime adapter about to be
|
|
22
22
|
* spawned declares it actually needs
|
|
23
|
-
* (`RuntimeAdapter.environmentRequirements
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* not by an extra check here.
|
|
23
|
+
* (`RuntimeAdapter.descriptor.environmentRequirements` — see
|
|
24
|
+
* `../types.ts`). A descriptor that declares no names gets the platform
|
|
25
|
+
* baseline only; descriptors are required and frozen before claim.
|
|
27
26
|
* 3. A per-device, per-runtime operator override (`DaemonConfig
|
|
28
27
|
* .runtimeEnvironment` — see `create-daemon.ts`) — a local escape hatch
|
|
29
28
|
* for a product/operator that knows it needs one more variable forwarded
|
|
@@ -39,8 +38,8 @@
|
|
|
39
38
|
*/
|
|
40
39
|
/**
|
|
41
40
|
* What one runtime adapter declares it needs beyond the always-included
|
|
42
|
-
* platform baseline.
|
|
43
|
-
* `RuntimeAdapter.environmentRequirements
|
|
41
|
+
* platform baseline. Declared in the required frozen
|
|
42
|
+
* `RuntimeAdapter.descriptor.environmentRequirements` (`../types.ts`).
|
|
44
43
|
*/
|
|
45
44
|
export interface RuntimeEnvironmentRequirements {
|
|
46
45
|
/**
|
|
@@ -69,8 +68,9 @@ export interface BuildRuntimeEnvOptions {
|
|
|
69
68
|
ambient: NodeJS.ProcessEnv;
|
|
70
69
|
/**
|
|
71
70
|
* The selected runtime adapter's own declared requirements —
|
|
72
|
-
* `undefined`
|
|
73
|
-
*
|
|
71
|
+
* `undefined` means "platform baseline only" for this helper. The public
|
|
72
|
+
* RuntimeAdapter descriptor always supplies this object before TaskRunner
|
|
73
|
+
* invokes the helper.
|
|
74
74
|
*/
|
|
75
75
|
requirements?: RuntimeEnvironmentRequirements;
|
|
76
76
|
/**
|
|
@@ -2,6 +2,27 @@ export interface GitWorkspaceConfig {
|
|
|
2
2
|
mode: 'local-checkpoints';
|
|
3
3
|
}
|
|
4
4
|
export type GitErrorCategory = 'git-unavailable' | 'git-timeout' | 'git-output-limit' | 'git-command-failed' | 'workspace-root-invalid' | 'workspace-root-conflict' | 'workspace-not-owned' | 'repository-root-mismatch' | 'repository-invalid' | 'lease-busy' | 'ledger-invalid';
|
|
5
|
+
/**
|
|
6
|
+
* Runtime projection of {@link GitErrorCategory}: every union member, once,
|
|
7
|
+
* in union order — the single source of truth the CLI's stable-output
|
|
8
|
+
* validators (`bin/format.ts`, `bin/audit-log.ts`, `bin/tasks-view.ts`,
|
|
9
|
+
* `bin/commands/workspaces.ts`) project from when deciding which category
|
|
10
|
+
* strings from ledger records are stable enough to render, so those filters
|
|
11
|
+
* can never drift from the union. The `satisfies` half rejects a string
|
|
12
|
+
* that isn't a union member; the `AssertExhaustive` proof below rejects a
|
|
13
|
+
* union member missing from this list — extending either side alone is a
|
|
14
|
+
* compile error. The runtime half (no duplicates, every consumer projects
|
|
15
|
+
* exactly this list) is `__tests__/git-category-drift.test.ts`.
|
|
16
|
+
*/
|
|
17
|
+
export declare const GIT_ERROR_CATEGORIES: readonly ["git-unavailable", "git-timeout", "git-output-limit", "git-command-failed", "workspace-root-invalid", "workspace-root-conflict", "workspace-not-owned", "repository-root-mismatch", "repository-invalid", "lease-busy", "ledger-invalid"];
|
|
18
|
+
/**
|
|
19
|
+
* Runtime projection of `GitWorkspacePhase` (the type itself lives in
|
|
20
|
+
* `git-workspace-store.ts`; the projection lives here beside
|
|
21
|
+
* {@link GIT_ERROR_CATEGORIES} so both category/phase single sources ship
|
|
22
|
+
* from one module) — same exhaustiveness contract, consumed by
|
|
23
|
+
* `bin/tasks-view.ts`'s phase filter.
|
|
24
|
+
*/
|
|
25
|
+
export declare const GIT_WORKSPACE_PHASES: readonly ["preparing", "active", "completed", "failed", "cancelled", "interrupted", "salvage"];
|
|
5
26
|
export declare class GitWorkspaceError extends Error {
|
|
6
27
|
readonly category: GitErrorCategory;
|
|
7
28
|
constructor(category: GitErrorCategory, message?: string);
|