@byok-sdk/cloud 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 CHANGED
@@ -4,6 +4,49 @@ Stateless hosted BYOK HTTP handlers and an in-memory reference composition over
4
4
  tenant-first `@byok-sdk/core` ports. It owns device-facing protocol/auth/policy
5
5
  logic but no durable database or object-storage driver.
6
6
 
7
- Pair it with `@byok-sdk/cloud-postgres` for Postgres + R2 production storage.
7
+ Pair it with `@byok-sdk/cloud-dataplane` for Postgres + R2 production storage.
8
+
9
+ Hosted compositions enqueue the distinct toolset offer message explicitly:
10
+
11
+ ```ts
12
+ await cloud.enqueueToolsetOffer(tenantId, deviceId, {
13
+ taskId,
14
+ payload: {
15
+ instruction: 'Research the account and prepare the next sales action.',
16
+ runtime: 'claude',
17
+ policy: { mode: 'auto' },
18
+ requiredToolsets: ['salesko.prospecting'],
19
+ },
20
+ });
21
+ ```
22
+
23
+ Unlike the live self-hosted coordinator, this stateless enqueue API cannot
24
+ infer current device capabilities; the host must route to a device known to
25
+ advertise `toolset-selection`. `listPresence(tenant)` includes the optional
26
+ `configuredToolsets` reported by each live daemon, so the host can narrow
27
+ candidate devices before enqueue. This is TTL-bounded discovery, not execution
28
+ authority: the daemon still resolves every required ID locally and declines
29
+ fail-closed if its configuration changed.
30
+
31
+ Reading a task's outcome goes through the same first terminal fact twice:
32
+ `readTerminalReceipt(tenant, taskId)` returns the stored envelope raw, and
33
+ `readTaskResult(tenant, taskId)` decodes that same receipt into a typed
34
+ `TerminalResult` — the state, plus `summary`/`sessionRef`/`artifactRefs`/
35
+ `document` on a completion or `reason`/`retryable` on a failure — projected
36
+ verbatim with no re-validation:
37
+
38
+ ```ts
39
+ const result = await cloud.readTaskResult(tenantId, taskId);
40
+ if (result === undefined) {
41
+ // No terminal fact yet. A declined task records none — read the attempt
42
+ // status with `readTaskAttempt(tenant, taskId)` for that case.
43
+ } else if (result.state === 'failed' && result.retryable) {
44
+ // re-offer
45
+ }
46
+ ```
47
+
48
+ `document` is absent, never null, when the daemon sent none; a receipt whose
49
+ stored body is not a terminal envelope throws `ByokCloudError` rather than
50
+ returning a best-effort shape.
8
51
 
9
52
  MIT licensed. Node.js 22.19.0 or newer.
@@ -1,22 +1,3 @@
1
- /**
2
- * The one nonce-signature check on the hosted surface (docs/protocol.md §6.2).
3
- *
4
- * S1 (GAP-004): a device signs `byok-nonce-v1\n` + nonce, never the bare
5
- * nonce. The device key is a long-lived identity key that later planes (S6
6
- * device proof) also sign structured messages with; without a domain tag, a
7
- * signature produced for one purpose is a valid signature for another.
8
- *
9
- * Applying the domain HERE rather than at the call site is the point: there
10
- * is exactly one place that decides what a device signature over a nonce
11
- * means, so no route can be written that accepts the undomained form. There
12
- * is deliberately no dual mode and no grace window — a device on the old
13
- * encoding re-pairs.
14
- *
15
- * The literal is byte-identical to `@byok-sdk/server`'s `NONCE_SIGNING_DOMAIN`
16
- * and to what the daemon signs (`packages/client/src/daemon/device-keys.ts`),
17
- * because the daemon must not be able to tell self-hosted from hosted. Parity
18
- * is asserted by behavior tests, never by importing the server.
19
- */
20
1
  import type { CloudCrypto } from '../crypto/port';
21
- export declare const NONCE_SIGNING_DOMAIN = "byok-nonce-v1\n";
2
+ export { NONCE_SIGNING_DOMAIN } from '@byok-sdk/core';
22
3
  export declare function verifyNonceSignature(crypto: CloudCrypto, devicePublicKey: string, nonce: string, signature: string): Promise<boolean>;
@@ -49,6 +49,18 @@ export declare const CLOUD_CAPABILITIES: {
49
49
  readonly activityTail: 'activity.tail';
50
50
  /** Request-bound device proof record manifest/read/write surface (S6). */
51
51
  readonly truthRecords: 'truth.records';
52
+ /**
53
+ * Tenant-scoped skill pack distribution: the manifest list plus the per-file
54
+ * content route a paired device installs from.
55
+ *
56
+ * Hosted HTTP on purpose. A pack is DECLARATIVE CONTENT a device pulls after
57
+ * reading this declaration — not a message — so `@byok-sdk/protocol` gains
58
+ * nothing from it and the frozen v1 envelope stays untouched. Composition-
59
+ * bound like `truth.records`: a deployment that declares it without supplying
60
+ * a `SkillPackStore` is refused at construction rather than publishing two
61
+ * routes it would then 404.
62
+ */
63
+ readonly skillPacks: 'skills.pack';
52
64
  };
53
65
  export type CloudCapability = (typeof CLOUD_CAPABILITIES)[keyof typeof CLOUD_CAPABILITIES];
54
66
  /** The wire DTO for `GET /byok/capabilities` — core's shape, bound to a cloud-owned route. */
@@ -65,6 +77,12 @@ export interface FullCapabilityDeclarationOptions {
65
77
  * composition cannot truthfully promise a cross-store atomic commit.
66
78
  */
67
79
  readonly includeTruthRecords?: boolean;
80
+ /**
81
+ * `skills.pack` is omitted for the same reason: the standard composition is
82
+ * given no `SkillPackStore`, and a default-on declaration would make every
83
+ * existing deployment refuse to construct.
84
+ */
85
+ readonly includeSkillPacks?: boolean;
68
86
  }
69
87
  /** Every capability the standard composition can serve, plus explicitly wired composition-bound ones. */
70
88
  export declare function fullCapabilityDeclaration(version?: number, options?: FullCapabilityDeclarationOptions): CapabilityDeclaration;
package/dist/cloud.d.ts CHANGED
@@ -14,12 +14,13 @@
14
14
  * quietly undo that — a Running/session map — is asserted absent by
15
15
  * `src/__tests__/constraints.test.ts`.
16
16
  */
17
- import { type ActivityTail, type BoardItem, type BoardItemInput, type BoardListQuery, type BoardPage, type CapabilityDeclaration, type Clock, type CoreStores, type PresenceHint, type TenantId } from '@byok-sdk/core';
18
- import { type Envelope, type TaskOfferPayload } from '@byok-sdk/protocol';
17
+ import { type ActivityTail, type BoardItem, type BoardItemInput, type BoardListQuery, type BoardPage, type CapabilityDeclaration, type Clock, type CoreStores, type PresenceHint, type SkillPackStore, type TenantId } from '@byok-sdk/core';
18
+ import { type Envelope, type TaskOfferPayload, type TaskOfferWithToolsetsPayload } from '@byok-sdk/protocol';
19
19
  import type { TokenSigner } from './auth/tokens';
20
20
  import type { CloudCrypto } from './crypto/port';
21
21
  import { type RouteDescriptor } from './router/registry';
22
22
  import type { BlobContentProxy, CloudStores, DeviceRecord, PairingCodeInfo, RequestReceipt, TaskAttempt } from './stores/ports';
23
+ import { type TerminalResult } from './terminal-result';
23
24
  import type { TruthCommitter, TruthObjectDownloads } from './truth/contract';
24
25
  /** Matches the reference server's ceiling (§7). */
25
26
  export declare const DEFAULT_MAX_BLOB_SIZE_BYTES: number;
@@ -45,6 +46,14 @@ export interface ByokCloudOptions {
45
46
  readonly truthCommitter?: TruthCommitter;
46
47
  /** Content-hash keyed object GET grants for object-backed truth bodies. */
47
48
  readonly truthObjectDownloads?: TruthObjectDownloads;
49
+ /**
50
+ * Tenant-scoped skill pack catalogue. OPTIONAL, and absent is a first-class
51
+ * answer: a deployment that distributes no declarative content supplies
52
+ * nothing here and declares no `skills.pack`. Supplying it is necessary but
53
+ * not sufficient for the routes to exist — the deployment must also declare
54
+ * the capability (ADR-010), the same asymmetry the byte proxy above has.
55
+ */
56
+ readonly skillPacks?: SkillPackStore;
48
57
  readonly crypto: CloudCrypto;
49
58
  readonly tokenSigner: TokenSigner;
50
59
  readonly clock: Clock;
@@ -71,12 +80,19 @@ export interface ByokCloudOptions {
71
80
  readonly activityCapacity?: number;
72
81
  readonly activityTtlMs?: number;
73
82
  readonly maxTruthRequestBytes?: number;
83
+ readonly skillPackPageLimit?: number;
74
84
  }
75
85
  export interface EnqueueOfferInput {
76
86
  /** Supply one to make the enqueue addressable by the host's own id; otherwise cloud mints one. */
77
87
  readonly taskId?: string;
78
88
  readonly payload: TaskOfferPayload;
79
89
  }
90
+ export interface EnqueueToolsetOfferInput {
91
+ /** Supply one to make the enqueue addressable by the host's own id; otherwise cloud mints one. */
92
+ readonly taskId?: string;
93
+ /** Strict control payload containing logical ids only; executable MCP definitions are not part of this type. */
94
+ readonly payload: TaskOfferWithToolsetsPayload;
95
+ }
80
96
  export interface EnqueuedOffer {
81
97
  readonly taskId: string;
82
98
  /** The per-(tenant, device) delivery seq — the daemon's redelivery cursor position for this envelope. */
@@ -107,9 +123,24 @@ export interface ByokCloud {
107
123
  }): Promise<PairingCodeInfo>;
108
124
  /** Host control plane: hand a device a frozen-v1 `task.offer`. The hosted replacement for `dispatch()` — a function, not a handle. */
109
125
  enqueueOffer(tenant: TenantId, deviceId: string, input: EnqueueOfferInput): Promise<EnqueuedOffer>;
126
+ /** Host control plane: enqueue the additive fail-closed offer variant that requires local MCP toolsets. */
127
+ enqueueToolsetOffer(tenant: TenantId, deviceId: string, input: EnqueueToolsetOfferInput): Promise<EnqueuedOffer>;
110
128
  readTaskAttempt(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
111
129
  /** The recorded terminal for a task — the first one, re-encoded canonically under the frozen v1 codec (see `recordTerminal`, `inbound.ts`: the stored body is `encodeEnvelope` of the zod-parsed envelope, not the device's original byte sequence). */
112
130
  readTerminalReceipt(tenant: TenantId, taskId: string): Promise<RequestReceipt | undefined>;
131
+ /**
132
+ * Host control plane: the same first terminal, decoded into the typed read
133
+ * model ({@link TerminalResult}) so a host reads result fields, not envelope
134
+ * prose. `undefined` ONLY means no terminal fact is recorded yet:
135
+ * first-terminal-wins is inherited from the receipt store
136
+ * ({@link ByokCloud.readTerminalReceipt} reads the same row), and a declined
137
+ * task records no terminal at all — use {@link ByokCloud.readTaskAttempt}
138
+ * for that attempt status. An absent `document` covers both a legacy
139
+ * pre-`result-document` daemon build and a daemon with no `resultDocument`
140
+ * extractor; a receipt whose body is not a terminal envelope throws rather
141
+ * than returning a best-effort shape.
142
+ */
143
+ readTaskResult(tenant: TenantId, taskId: string): Promise<TerminalResult | undefined>;
113
144
  listDevices(tenant: TenantId): Promise<readonly DeviceRecord[]>;
114
145
  revokeDevice(tenant: TenantId, deviceId: string): Promise<void>;
115
146
  /** Host control plane: create a board row from explicit producer labels. */
@@ -7,7 +7,7 @@
7
7
  * cannot tell this from `@byok-sdk/server`, and nothing in the path needs a
8
8
  * database to prove it.
9
9
  */
10
- import { type Clock, type CapabilityDeclaration, type CoreStores } from '@byok-sdk/core';
10
+ import { type CapabilityDeclaration, type Clock, type CoreStores, type SkillPackStore } from '@byok-sdk/core';
11
11
  import { type TokenSigner } from '../auth/tokens';
12
12
  import type { CloudCrypto } from '../crypto/port';
13
13
  import { type ByokCloud } from '../cloud';
@@ -41,6 +41,14 @@ export interface InMemoryByokCloudOptions {
41
41
  readonly truthCommitter?: TruthCommitter;
42
42
  readonly truthObjectDownloads?: TruthObjectDownloads;
43
43
  readonly maxTruthRequestBytes?: number;
44
+ /**
45
+ * Host/test supplied skill pack catalogue. Absent by default, and
46
+ * `fullCapabilityDeclaration()` withholds `skills.pack` to match — a
47
+ * composition that declared a channel it has no store for would be refused at
48
+ * construction, which would break every existing in-memory deployment.
49
+ */
50
+ readonly skillPacks?: SkillPackStore;
51
+ readonly skillPackPageLimit?: number;
44
52
  }
45
53
  export interface InMemoryByokCloud {
46
54
  readonly cloud: ByokCloud;
package/dist/errors.d.ts CHANGED
@@ -13,11 +13,9 @@ export declare const CLOUD_ERROR_CODES: {
13
13
  /** The composition handed a device row whose tenant is not a mintable `TenantId`. */
14
14
  readonly device_tenant_invalid: 'device_tenant_invalid';
15
15
  /**
16
- * The mailbox assigned a row `seq` that disagrees with the delivery `seq`
17
- * baked into the enqueued envelope. Loud rather than silent: those two
18
- * numbers ARE the daemon's redelivery cursor, and a composition whose
19
- * mailbox numbers rows differently from `DeviceSequenceStore` would
20
- * mis-deliver every subsequent poll.
16
+ * The mailbox committed a row `seq` that disagrees with the delivery `seq`
17
+ * its body factory baked into the envelope. Loud rather than silent: those
18
+ * two numbers ARE the daemon's redelivery cursor.
21
19
  */
22
20
  readonly mailbox_seq_mismatch: 'mailbox_seq_mismatch';
23
21
  /** A capability declaration the host supplied that core refused. */
@@ -34,6 +32,13 @@ export declare const CLOUD_ERROR_CODES: {
34
32
  readonly capability_over_declared: 'capability_over_declared';
35
33
  /** Host-supplied board labels or coordination input exceeded the explicit contract. */
36
34
  readonly coordination_input_invalid: 'coordination_input_invalid';
35
+ /**
36
+ * A terminal receipt whose stored body is not a terminal envelope — either
37
+ * undecodable or a non-terminal type. Whatever wrote that row broke the
38
+ * receipt-store contract, so the typed read model fails closed instead of
39
+ * projecting a best-effort shape.
40
+ */
41
+ readonly terminal_receipt_unreadable: 'terminal_receipt_unreadable';
37
42
  /** A progress/activity batch exceeded the configured event or byte ceiling. */
38
43
  readonly activity_batch_too_large: 'activity_batch_too_large';
39
44
  };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * `GET /byok/skill-packs` and `GET /byok/skill-packs/:name/files/:path` — the
3
+ * hosted half of the skill-pack delivery channel.
4
+ *
5
+ * Two device-class reads, and nothing else. There is no publish route here on
6
+ * purpose: a device is a CONSUMER of packs, and a device-bearer-authed write
7
+ * would let any paired device in a tenant publish content to every other device
8
+ * in it. Publication is a host control-plane action against the store directly,
9
+ * exactly as board item creation is.
10
+ *
11
+ * Both routes answer 404 for a pack this tenant does not have — the same answer
12
+ * a name that never existed anywhere gets — so the surface is not a
13
+ * cross-tenant existence oracle. The store is tenant-first, so that property
14
+ * comes from the lookup key rather than from a comparison a handler could
15
+ * forget.
16
+ *
17
+ * Bytes travel as UTF-8 text inside JSON. A pack carries Markdown, YAML and
18
+ * static text; there is no archive to unpack and no binary channel to
19
+ * negotiate, which is the same reason the manifest has no exec surface — the
20
+ * format cannot express the thing we do not want it to express.
21
+ */
22
+ import { type SkillPackStore } from '@byok-sdk/core';
23
+ import type { Context } from 'hono';
24
+ import { type BearerAuthDeps } from '../auth/bearer';
25
+ /** Rows per `GET /byok/skill-packs` response. A tenant's pack catalogue is small by design. */
26
+ export declare const DEFAULT_SKILL_PACK_PAGE_LIMIT = 50;
27
+ export interface SkillPackRouteDeps {
28
+ readonly bearer: BearerAuthDeps;
29
+ readonly skillPacks: SkillPackStore;
30
+ readonly pageLimit: number;
31
+ }
32
+ export declare function skillPackListHandler(deps: SkillPackRouteDeps): (c: Context) => Promise<Response>;
33
+ export declare function skillPackFileHandler(deps: SkillPackRouteDeps): (c: Context) => Promise<Response>;
@@ -1,7 +1,8 @@
1
+ import { DEVICE_PROOF_HEADER } from '@byok-sdk/core';
1
2
  import type { Context } from 'hono';
2
3
  import { type DeviceProofAuthDeps } from '../auth/device-proof';
3
4
  import { type TruthCommitter, type TruthObjectDownloads } from '../truth/contract';
4
- export declare const DEVICE_PROOF_HEADER = "x-byok-device-proof";
5
+ export { DEVICE_PROOF_HEADER };
5
6
  export declare const DEFAULT_MAX_TRUTH_REQUEST_BYTES: number;
6
7
  export declare const MAX_DEVICE_PROOF_HEADER_BYTES: number;
7
8
  export interface TruthRouteDeps {
package/dist/index.d.ts CHANGED
@@ -15,9 +15,10 @@
15
15
  export { isTenantId, tenantId } from '@byok-sdk/core';
16
16
  export type { TenantId } from '@byok-sdk/core';
17
17
  export { createByokCloud } from './cloud';
18
- export type { ByokCloud, ByokCloudOptions, EnqueueOfferInput, EnqueuedOffer } from './cloud';
18
+ export type { ByokCloud, ByokCloudOptions, EnqueueOfferInput, EnqueueToolsetOfferInput, EnqueuedOffer, } from './cloud';
19
19
  export { DEFAULT_EVENTS_PAGE_LIMIT, DEFAULT_LONG_POLL_HOLD_MS, DEFAULT_LONG_POLL_INTERVAL_MS, DEFAULT_MAX_BLOB_SIZE_BYTES, } from './cloud';
20
20
  export { DEFAULT_BOARD_PAGE_LIMIT, DEFAULT_BOARD_STREAM_HEARTBEAT_INTERVAL_MS, DEFAULT_BOARD_STREAM_QUERY_INTERVAL_MS, DEFAULT_BOARD_STREAM_RECONCILIATION_INTERVAL_MS, } from './handlers/board';
21
+ export { DEFAULT_SKILL_PACK_PAGE_LIMIT } from './handlers/skill-packs';
21
22
  export { createInMemoryByokCloud } from './composition/in-memory';
22
23
  export type { InMemoryByokCloud, InMemoryByokCloudOptions } from './composition/in-memory';
23
24
  export { ByokCloudError, CLOUD_ERROR_CODES, isCloudError } from './errors';
@@ -39,8 +40,10 @@ export { createWebCrypto } from './crypto/web-crypto';
39
40
  export type { CloudCrypto } from './crypto/port';
40
41
  export { handleInboundEnvelope, terminalReceiptKey } from './inbound';
41
42
  export type { InboundOutcome } from './inbound';
43
+ export { projectTerminalResult } from './terminal-result';
44
+ export type { TerminalResult } from './terminal-result';
42
45
  export { tenantStoresFor } from './tenant-stores';
43
- export type { TenantBoundActivity, TenantBoundBoard, CloudRootStores, TenantBoundBlobs, TenantBoundDedup, TenantBoundDevices, TenantBoundMailbox, TenantBoundPresence, TenantBoundQuota, TenantBoundRateLimiter, TenantBoundReceipts, TenantBoundSequence, TenantBoundTaskAttempts, TenantStores, } from './tenant-stores';
46
+ export type { TenantBoundActivity, TenantBoundBoard, CloudRootStores, TenantBoundBlobs, TenantBoundDedup, TenantBoundDevices, TenantBoundMailbox, TenantBoundPresence, TenantBoundQuota, TenantBoundRateLimiter, TenantBoundReceipts, TenantBoundTaskAttempts, TenantStores, } from './tenant-stores';
44
47
  export { DEFAULT_ACTIVITY_BOUNDS, DEFAULT_ACTIVITY_CAPACITY, DEFAULT_ACTIVITY_MAX_BYTES, DEFAULT_ACTIVITY_MAX_EVENTS, DEFAULT_ACTIVITY_TTL_MS, DEFAULT_BOARD_CHANNEL_MAX_BYTES, DEFAULT_BOARD_TITLE_MAX_BYTES, DEFAULT_PRESENCE_DETAIL_MAX_BYTES, DEFAULT_PRESENCE_MINIMUM_INTERVAL_MS, DEFAULT_PRESENCE_TTL_MS, } from './coordination';
45
48
  export type { ActivityBounds } from './coordination';
46
49
  export { BoardFeedClient, BoardFeedRetryableError, BoardFeedStoppedError } from './coordination-client';
@@ -52,6 +55,6 @@ export { TruthCommitError, isTruthCommitError } from './truth/errors';
52
55
  export type { TruthCommitErrorCode } from './truth/errors';
53
56
  export { CLOUD_STORE_NAMES, TASK_ATTEMPT_STATUSES } from './stores/ports';
54
57
  export { CLOUD_PORT_INTERFACES, CLOUD_PORT_METHODS } from './stores/ports-contract';
55
- export type { BlobContent, BlobContentProxy, BlobDeclaration, BlobObservation, BlobWriteResult, CloudBlobStore, CloudStoreName, CloudStores, DeviceDirectory, DeviceRecord, DeviceRegistration, DeviceSequenceStore, InboundDedupStore, InboundRateLimiter, NonceStore, PairingCodeClaims, PairingCodeInfo, PairingCodeIssueInput, PairingCodeStore, ProofRequestReceipt, ProofRequestReceiptInput, ProofRequestReceiptStore, RequestReceipt, RequestReceiptStore, TaskAttempt, TaskAttemptStatus, TaskAttemptStore, } from './stores/ports';
56
- export { AllowAllRateLimiter, BLOB_URL_TTL_MS, DEDUP_RING_CAPACITY, InMemoryBlobContentProxy, InMemoryCloudBlobStore, InMemoryDeviceDirectory, InMemoryDeviceSequenceStore, InMemoryInboundDedupStore, InMemoryNonceStore, InMemoryPairingCodeStore, InMemoryRequestReceiptStore, InMemoryProofRequestReceiptStore, InMemoryTaskAttemptStore, NONCE_TTL_MS, createInMemoryBlobs, createInMemoryCloudStores, } from './stores/in-memory/index';
58
+ export type { BlobContent, BlobContentProxy, BlobDeclaration, BlobObservation, BlobWriteResult, CloudBlobStore, CloudStoreName, CloudStores, DeviceDirectory, DeviceRecord, DeviceRegistration, InboundDedupStore, InboundRateLimiter, NonceStore, PairingCodeClaims, PairingCodeInfo, PairingCodeIssueInput, PairingCodeStore, ProofRequestReceipt, ProofRequestReceiptInput, ProofRequestReceiptStore, RequestReceipt, RequestReceiptStore, TaskAttempt, TaskAttemptStatus, TaskAttemptStore, } from './stores/ports';
59
+ export { AllowAllRateLimiter, BLOB_URL_TTL_MS, DEDUP_RING_CAPACITY, InMemoryBlobContentProxy, InMemoryCloudBlobStore, InMemoryDeviceDirectory, InMemoryInboundDedupStore, InMemoryNonceStore, InMemoryPairingCodeStore, InMemoryRequestReceiptStore, InMemoryProofRequestReceiptStore, InMemoryTaskAttemptStore, NONCE_TTL_MS, createInMemoryBlobs, createInMemoryCloudStores, } from './stores/in-memory/index';
57
60
  export type { InMemoryBlobStoreOptions, InMemoryBlobs, InMemoryCloudComposition, } from './stores/in-memory/index';