@byok-sdk/cloud-dataplane 0.11.0 → 0.13.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.
@@ -0,0 +1,20 @@
1
+ -- 0016_mailbox_delivery_watermark.sql — server-owned mailbox delivery bound.
2
+ --
3
+ -- Client acknowledgement is allowed only through the highest cursor the
4
+ -- server has returned to that exact tenant/device. Enqueue allocation is not
5
+ -- delivery authority: next_seq may be far ahead while no response has exposed
6
+ -- those rows. Existing acknowledged rows are the one safe backfill fact — an
7
+ -- acknowledgement already persisted before this migration necessarily came
8
+ -- from the historical delivery path.
9
+
10
+ ALTER TABLE device_stream
11
+ ADD COLUMN delivered_seq bigint NOT NULL DEFAULT 0;
12
+
13
+ UPDATE device_stream
14
+ SET delivered_seq = acked_seq;
15
+
16
+ ALTER TABLE device_stream
17
+ ADD CONSTRAINT device_stream_delivery_nonnegative
18
+ CHECK (delivered_seq >= 0),
19
+ ADD CONSTRAINT device_stream_ack_within_delivery
20
+ CHECK (acked_seq <= delivered_seq);
@@ -0,0 +1,23 @@
1
+ -- Durable first-message admission serialized with task cancellation.
2
+ --
3
+ -- This one row records both the admission and the immutable final disposition:
4
+ -- exactly one new agent.message.publish may cross from a live task into the
5
+ -- product consumer. A pending row is deliberately fail-closed: elapsed time
6
+ -- cannot prove that an external consumer stopped, so retries never invoke it
7
+ -- again or synthesize a terminal result. The task row is locked by the
8
+ -- application transaction before inserting here, so cancellation and first
9
+ -- message admission have one durable winner.
10
+
11
+ CREATE TABLE agent_message_admission (
12
+ tenant_id text NOT NULL,
13
+ device_id text NOT NULL,
14
+ task_id text NOT NULL,
15
+ message_id text NOT NULL,
16
+ payload_body text NOT NULL,
17
+ terminal_body text,
18
+ PRIMARY KEY (tenant_id, device_id, task_id),
19
+ UNIQUE (tenant_id, device_id, message_id),
20
+ FOREIGN KEY (tenant_id, task_id)
21
+ REFERENCES task (tenant_id, task_id)
22
+ ON DELETE CASCADE
23
+ );
@@ -0,0 +1,23 @@
1
+ -- The claim-time runtime snapshot on the task attempt.
2
+ --
3
+ -- Two columns, one fact: what the daemon that WON the ownership CAS reported
4
+ -- about itself on its own `task.claim` — its runtime id and that adapter's own
5
+ -- capability block. Both are written by the guarded claim UPDATE and by nothing
6
+ -- else, so they are set exactly once, at the `offered -> claimed` transition,
7
+ -- and a redelivered or losing claim can never restamp them.
8
+ --
9
+ -- They are ATTRIBUTION, not execution state: they stop changing the moment
10
+ -- ownership is decided, which is what keeps them inside what ADR-028 allows the
11
+ -- coordination plane to hold. `ByokCloud.steerTask` reads
12
+ -- `claimed_runtime_capabilities` as the single input to its steer gate; the
13
+ -- connection-level capability snapshot on the device row is deliberately not an
14
+ -- input, because it describes a device build rather than the adapter that took
15
+ -- this task.
16
+ --
17
+ -- Additive and nullable with NO backfill, deliberately. A task claimed before
18
+ -- this migration has no snapshot to reconstruct — the claim that carried it is
19
+ -- long gone — and inventing one would open the steer gate on a guess. NULL
20
+ -- means "unknown", and the gate refuses an unknown.
21
+
22
+ ALTER TABLE task ADD COLUMN claimed_runtime text;
23
+ ALTER TABLE task ADD COLUMN claimed_runtime_capabilities jsonb;
@@ -0,0 +1,36 @@
1
+ -- Strict Agent reliability identities.
2
+ --
3
+ -- A device may host many Agents. The old device-plus-envelope/event primary
4
+ -- keys therefore let Agent A suppress Agent B when they independently used
5
+ -- the same reliable id. This migration cuts those keys forward: physical facts
6
+ -- retain the empty Agent scope, while every Agent-bound caller must supply the
7
+ -- exact AgentRef. The empty pair is not an AgentRef and is never a read
8
+ -- fallback; it is the explicit keyspace for facts such as conn.hello that have
9
+ -- no Agent authority.
10
+
11
+ ALTER TABLE inbound_dedup
12
+ ADD COLUMN agent_id text NOT NULL DEFAULT '',
13
+ ADD COLUMN agent_profile_revision text NOT NULL DEFAULT '';
14
+
15
+ ALTER TABLE inbound_dedup
16
+ ADD CONSTRAINT inbound_dedup_agent_scope_shape CHECK (
17
+ (agent_id = '' AND agent_profile_revision = '') OR
18
+ (
19
+ octet_length(agent_id) BETWEEN 1 AND 160
20
+ AND octet_length(agent_profile_revision) BETWEEN 1 AND 160
21
+ AND agent_id !~ '[[:cntrl:]]'
22
+ AND agent_profile_revision !~ '[[:cntrl:]]'
23
+ )
24
+ );
25
+
26
+ ALTER TABLE inbound_dedup DROP CONSTRAINT inbound_dedup_pkey;
27
+ ALTER TABLE inbound_dedup
28
+ ADD PRIMARY KEY (tenant_id, device_id, agent_id, agent_profile_revision, envelope_id);
29
+
30
+ DROP INDEX inbound_dedup_reclaim_idx;
31
+ CREATE INDEX inbound_dedup_reclaim_idx
32
+ ON inbound_dedup (tenant_id, device_id, agent_id, agent_profile_revision, recorded_seq);
33
+
34
+ ALTER TABLE agent_egress_event DROP CONSTRAINT agent_egress_event_pkey;
35
+ ALTER TABLE agent_egress_event
36
+ ADD PRIMARY KEY (tenant_id, device_id, agent_id, agent_profile_revision, event_id);
@@ -0,0 +1,7 @@
1
+ -- Agent reliability identities are exact to the attempt's AgentRef. The
2
+ -- task-bound primary key already supplies that authority through `task`; a
3
+ -- device-wide message-id uniqueness constraint would instead let one Agent
4
+ -- suppress another active Agent on the same device.
5
+
6
+ ALTER TABLE agent_message_admission
7
+ DROP CONSTRAINT agent_message_admission_tenant_id_device_id_message_id_key;
@@ -1,3 +1,4 @@
1
+ import { type AgentEgressReliablePayload } from '@byok-sdk/protocol';
1
2
  import type { AgentEgressRecord, AgentEgressStore } from '@byok-sdk/cloud';
2
3
  import type { Clock, TenantId } from '@byok-sdk/core';
3
4
  import type { Pool } from 'pg';
@@ -10,5 +11,5 @@ export declare class PostgresAgentEgressStore implements AgentEgressStore {
10
11
  readonly record: AgentEgressRecord;
11
12
  readonly created: boolean;
12
13
  }>;
13
- get(tenant: TenantId, deviceId: string, eventId: string): Promise<AgentEgressRecord | undefined>;
14
+ get(tenant: TenantId, deviceId: string, agentRef: AgentEgressReliablePayload['agentRef'], eventId: string): Promise<AgentEgressRecord | undefined>;
14
15
  }
@@ -1,4 +1,4 @@
1
- import { type ApprovalTimelineAppendInput, type ApprovalTimelineStore, type ApprovalTimelineTail } from '@byok-sdk/cloud';
1
+ import { type ApprovalTimelineAppendInput, type ApprovalTimelineResolvePendingInput, type ApprovalTimelineResolvePendingResult, type ApprovalTimelineStore, type ApprovalTimelineTail } from '@byok-sdk/cloud';
2
2
  import type { Clock, TenantId } from '@byok-sdk/core';
3
3
  import type { Pool } from 'pg';
4
4
  export declare class PostgresApprovalTimelineStore implements ApprovalTimelineStore {
@@ -6,5 +6,6 @@ export declare class PostgresApprovalTimelineStore implements ApprovalTimelineSt
6
6
  private readonly clock;
7
7
  constructor(pool: Pool, clock: Clock);
8
8
  append(tenant: TenantId, input: ApprovalTimelineAppendInput): Promise<ApprovalTimelineTail>;
9
+ resolvePending(tenant: TenantId, input: ApprovalTimelineResolvePendingInput): Promise<ApprovalTimelineResolvePendingResult>;
9
10
  read(tenant: TenantId, taskId: string): Promise<ApprovalTimelineTail | undefined>;
10
11
  }
@@ -4,9 +4,9 @@
4
4
  * The load-bearing rule, and the one a composition can break silently:
5
5
  * **reading is not acknowledging.** `readAfter` is a `SELECT` and nothing else.
6
6
  * The only ack is `advanceCursor`, which the daemon calls after it has durably
7
- * journaled the envelope, and it is monotonic — a lower cursor is refused with
8
- * the cursor it lost to rather than quietly re-delivering work the device has
9
- * already run.
7
+ * journaled the envelope. It is monotonic and bounded by `recordDelivery`:
8
+ * regression and future cursors are refused with the cursor state they lost
9
+ * to rather than re-delivering or silently skipping work.
10
10
  *
11
11
  * `collectRetired` deletes acked rows and **marks** unacked ones `expired`. It
12
12
  * never deletes an unacked row. §12.7.5 requires an envelope that aged out
@@ -19,7 +19,7 @@
19
19
  * the SQL expression of §12.6.2 layer 3: there is no bare device index to
20
20
  * accidentally query.
21
21
  */
22
- import { type Clock, type MailboxAdvanceCursorInput, type MailboxAppendInput, type MailboxCursorState, type MailboxMessage, type MailboxPage, type MailboxReadQuery, type MailboxRetentionInput, type MailboxRetentionResult, type MailboxStore, type TenantId } from '@byok-sdk/core';
22
+ import { type Clock, type MailboxAdvanceCursorInput, type MailboxAppendInput, type MailboxCursorState, type MailboxMessage, type MailboxPage, type MailboxReadQuery, type MailboxRecordDeliveryInput, type MailboxRetentionInput, type MailboxRetentionResult, type MailboxStore, type TenantId } from '@byok-sdk/core';
23
23
  import type { Pool } from 'pg';
24
24
  export declare const OUTBOX_COLUMNS = "tenant_id, device_id, seq, message_id, body, body_hash, byte_size, state, appended_at";
25
25
  export interface OutboxRow {
@@ -40,6 +40,7 @@ export declare class PostgresMailboxStore implements MailboxStore {
40
40
  append(tenant: TenantId, input: MailboxAppendInput): Promise<MailboxMessage>;
41
41
  readAfter(tenant: TenantId, query: MailboxReadQuery): Promise<MailboxPage>;
42
42
  advanceCursor(tenant: TenantId, input: MailboxAdvanceCursorInput): Promise<MailboxCursorState>;
43
+ recordDelivery(tenant: TenantId, input: MailboxRecordDeliveryInput): Promise<MailboxCursorState>;
43
44
  readCursor(tenant: TenantId, deviceId: string): Promise<MailboxCursorState>;
44
45
  collectRetired(tenant: TenantId, input: MailboxRetentionInput): Promise<MailboxRetentionResult>;
45
46
  }
@@ -6,17 +6,20 @@
6
6
  * primary key does the deciding, and zero returned rows means "already seen".
7
7
  *
8
8
  * Reclaim runs only on the path that actually grew the table, and it deletes
9
- * oldest-first down to `DEDUP_RING_CAPACITY` rows for that device — the same
10
- * bound the in-memory ring holds. The ids most likely to be redelivered are the
11
- * recent ones, so dropping the oldest is the retention that matches the wire's
12
- * behavior. An unbounded set would pass every duplicate assertion and still let
13
- * one chatty device grow this table without limit.
9
+ * oldest-first down to `DEDUP_RING_CAPACITY` rows for that exact physical or
10
+ * Agent-bound key — the same bound the in-memory ring holds. The ids most
11
+ * likely to be redelivered are the recent ones, so dropping the oldest is the
12
+ * retention that matches the wire's behavior. An unbounded set would pass every
13
+ * duplicate assertion and still let one chatty device or Agent grow this table
14
+ * without limit.
14
15
  */
15
16
  import { type InboundDedupStore } from '@byok-sdk/cloud';
16
17
  import type { TenantId } from '@byok-sdk/core';
18
+ import type { AgentRef } from '@byok-sdk/protocol';
17
19
  import type { Pool } from 'pg';
18
20
  export declare class PostgresInboundDedupStore implements InboundDedupStore {
19
21
  #private;
20
22
  constructor(pool: Pool, capacity?: number);
21
23
  checkAndRecord(tenant: TenantId, deviceId: string, envelopeId: string): Promise<boolean>;
24
+ checkAndRecordAgent(tenant: TenantId, deviceId: string, agentRef: AgentRef, envelopeId: string): Promise<boolean>;
22
25
  }
@@ -19,7 +19,16 @@
19
19
  */
20
20
  import { type Clock, type PresenceStore, type TenantId, type TenantReadiness } from '@byok-sdk/core';
21
21
  import type { DeviceDirectory, DeviceRecord, DeviceRegistration } from '@byok-sdk/cloud';
22
- import type { Pool } from 'pg';
22
+ import type { Pool, PoolClient } from 'pg';
23
+ /**
24
+ * The client-scoped registration mutation shared by standalone device
25
+ * registration and pairing enrollment. Its caller owns the transaction so the
26
+ * guarded pairing-code update, machine supersession cleanup, and insert can
27
+ * commit or roll back together.
28
+ */
29
+ export declare function registerDeviceOnClient(client: PoolClient, tenant: TenantId, input: DeviceRegistration): Promise<DeviceRecord>;
30
+ /** Read one active device while a caller-owned transaction still holds its authority lock. */
31
+ export declare function getDeviceOnClient(client: PoolClient, tenant: TenantId, deviceId: string): Promise<DeviceRecord | undefined>;
23
32
  export declare class PostgresDeviceDirectory implements DeviceDirectory {
24
33
  #private;
25
34
  constructor(pool: Pool, clock?: Clock);
@@ -21,6 +21,5 @@ export declare class PostgresNonceStore implements NonceStore {
21
21
  #private;
22
22
  constructor(pool: Pool, clock: Clock, crypto: CloudCrypto, ttlMs?: number);
23
23
  issue(tenant: TenantId, deviceId: string): Promise<string>;
24
- validate(tenant: TenantId, deviceId: string, nonce: string): Promise<boolean>;
25
- markUsed(tenant: TenantId, nonce: string): Promise<void>;
24
+ consumeIfValid(tenant: TenantId, deviceId: string, nonce: string): Promise<boolean>;
26
25
  }
@@ -2,12 +2,11 @@
2
2
  * Postgres {@link PairingCodeStore}: single-use codes bound to the tenant and
3
3
  * product they were minted for.
4
4
  *
5
- * Redemption is one guarded statement. `UPDATE ... WHERE redeemed_at IS NULL
6
- * AND expires_at >= $now RETURNING ...` consumes and reports in the same
7
- * round trip, and zero rows is the typed rejection. A read-then-write would let
8
- * two concurrent redemptions both observe an unused code, and single-use is
9
- * exactly what makes the caller's "redeem, then register the device" sequence
10
- * exclusive.
5
+ * Enrollment is one transaction. `UPDATE ... WHERE redeemed_at IS NULL AND
6
+ * expires_at >= $now RETURNING ...` claims the code only inside the same client
7
+ * transaction that applies machine supersession/state cleanup and inserts the
8
+ * device. A read-then-write would let two callers observe an unused code; an
9
+ * autocommitted redemption would strand a code when registration fails.
11
10
  *
12
11
  * Unknown, expired, and already-used all answer `undefined`. The reference
13
12
  * server distinguishes them in its 401 text; a hosted multi-tenant surface
@@ -16,11 +15,19 @@
16
15
  * difference an attacker enumerating codes would pay for.
17
16
  */
18
17
  import type { Clock, TenantId } from '@byok-sdk/core';
19
- import type { PairingCodeClaims, PairingCodeInfo, PairingCodeIssueInput, PairingCodeStore } from '@byok-sdk/cloud';
18
+ import type { DeviceRecord, PairingCodeInfo, PairingCodeIssueInput, PairingCodeStore } from '@byok-sdk/cloud';
20
19
  import type { Pool } from 'pg';
21
20
  export declare class PostgresPairingCodeStore implements PairingCodeStore {
22
21
  #private;
23
22
  constructor(pool: Pool, clock: Clock);
24
23
  issue(tenant: TenantId, input: PairingCodeIssueInput): Promise<PairingCodeInfo>;
25
- redeem(code: string): Promise<PairingCodeClaims | undefined>;
24
+ redeemAndRegister(input: {
25
+ readonly pairingCode: string;
26
+ readonly deviceId: string;
27
+ readonly deviceName: string;
28
+ readonly devicePublicKey: string;
29
+ readonly proofKeyId: string;
30
+ readonly proofKeyEpoch: number;
31
+ readonly machineId?: string;
32
+ }): Promise<DeviceRecord | undefined>;
26
33
  }
@@ -14,8 +14,9 @@
14
14
  * `claim` and `recordStatus` on a task this tenant never offered write nothing
15
15
  * and return `undefined`.
16
16
  */
17
- import type { AgentRef, TaskAttempt, TaskAttemptStatus, TaskAttemptStore } from '@byok-sdk/cloud';
17
+ import { type AgentMessageAdmission, type AgentRef, type TaskAttempt, type TaskAttemptListQuery, type TaskAttemptPage, type TaskAttemptStatus, type TaskAttemptStore } from '@byok-sdk/cloud';
18
18
  import type { Clock, TenantId } from '@byok-sdk/core';
19
+ import type { RuntimeCapabilities, RuntimeId } from '@byok-sdk/protocol';
19
20
  import type { Pool } from 'pg';
20
21
  export interface TaskRow {
21
22
  readonly tenant_id: string;
@@ -29,9 +30,11 @@ export interface TaskRow {
29
30
  readonly cancel_requested_at: Date | null;
30
31
  readonly cancel_reason: string | null;
31
32
  readonly cancel_message_id: string | null;
33
+ readonly claimed_runtime: string | null;
34
+ readonly claimed_runtime_capabilities: RuntimeCapabilities | null;
32
35
  readonly updated_at: Date;
33
36
  }
34
- export declare const TASK_SELECT_COLUMNS = "tenant_id, task_id, device_id, agent_id, agent_profile_revision, owner_device_id, status, terminal_cause, cancel_requested_at, cancel_reason, cancel_message_id, updated_at";
37
+ export declare const TASK_SELECT_COLUMNS = "tenant_id, task_id, device_id, agent_id, agent_profile_revision, owner_device_id, status, terminal_cause, cancel_requested_at, cancel_reason, cancel_message_id, claimed_runtime, claimed_runtime_capabilities, updated_at";
35
38
  export declare function taskRowToAttempt(row: TaskRow): TaskAttempt;
36
39
  export declare class PostgresTaskAttemptStore implements TaskAttemptStore {
37
40
  #private;
@@ -49,11 +52,33 @@ export declare class PostgresTaskAttemptStore implements TaskAttemptStore {
49
52
  readonly attempt: TaskAttempt;
50
53
  readonly created: boolean;
51
54
  }>;
55
+ reserveAgentMessage(tenant: TenantId, input: {
56
+ readonly taskId: string;
57
+ readonly deviceId: string;
58
+ readonly messageId: string;
59
+ readonly payloadBody: string;
60
+ }): Promise<'reserved' | 'pending' | 'rejected'>;
61
+ readAgentMessage(tenant: TenantId, input: {
62
+ readonly taskId: string;
63
+ readonly deviceId: string;
64
+ readonly messageId: string;
65
+ readonly payloadBody: string;
66
+ }): Promise<AgentMessageAdmission | undefined>;
67
+ finalizeAgentMessage(tenant: TenantId, input: {
68
+ readonly taskId: string;
69
+ readonly deviceId: string;
70
+ readonly messageId: string;
71
+ readonly payloadBody: string;
72
+ readonly terminalBody: string;
73
+ }): Promise<AgentMessageAdmission | undefined>;
52
74
  get(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
53
75
  getMany(tenant: TenantId, taskIds: readonly string[]): Promise<readonly TaskAttempt[]>;
76
+ list(tenant: TenantId, query: TaskAttemptListQuery): Promise<TaskAttemptPage>;
54
77
  claim(tenant: TenantId, input: {
55
78
  readonly taskId: string;
56
79
  readonly deviceId: string;
80
+ readonly runtime?: RuntimeId;
81
+ readonly capabilities?: RuntimeCapabilities;
57
82
  }): Promise<TaskAttempt | undefined>;
58
83
  recordStatus(tenant: TenantId, input: {
59
84
  readonly taskId: string;
@@ -1,8 +1,8 @@
1
1
  import { type Clock, type TenantId } from '@byok-sdk/core';
2
2
  import type { Pool } from 'pg';
3
3
  import { type R2BlobStoreOptions, type R2ObjectMaintenance } from './stores/r2-blobs';
4
- /** Every tenant-owned table as of 0014, in child-before-parent deletion order. */
5
- export declare const TENANT_ERASURE_TABLES: readonly ['object_reference', 'object_manifest', 'storage_reservation', 'storage_usage', 'storage_entitlement', 'gc_cursor', 'cleanup_job', 'tenant_retention_policy', 'skill_pack_file', 'skill_pack', 'approval_timeline_tail', 'activity_tail', 'attested_record', 'board_item', 'tenant_stream', 'outbox', 'agent_memory_projection_metering_receipt', 'agent_memory_projection_head', 'agent_memory_projection_erase_fence', 'agent_egress_event', 'device_request_receipts', 'proof_request_receipt', 'task', 'device_presence', 'device_assertion_replay', 'device_stream', 'inbound_dedup', 'auth_nonce', 'pairing_code', 'device'];
4
+ /** Every tenant-owned table as of 0017, in child-before-parent deletion order. */
5
+ export declare const TENANT_ERASURE_TABLES: readonly ['object_reference', 'object_manifest', 'storage_reservation', 'storage_usage', 'storage_entitlement', 'gc_cursor', 'cleanup_job', 'tenant_retention_policy', 'skill_pack_file', 'skill_pack', 'approval_timeline_tail', 'activity_tail', 'attested_record', 'board_item', 'tenant_stream', 'outbox', 'agent_memory_projection_metering_receipt', 'agent_memory_projection_head', 'agent_memory_projection_erase_fence', 'agent_egress_event', 'device_request_receipts', 'proof_request_receipt', 'agent_message_admission', 'task', 'device_presence', 'device_assertion_replay', 'device_stream', 'inbound_dedup', 'auth_nonce', 'pairing_code', 'device'];
6
6
  export declare const TENANT_ERASURE_ERROR_CODES: {
7
7
  readonly tenant_erasure_invalid_input: 'tenant_erasure_invalid_input';
8
8
  readonly tenant_erasure_schema_drift: 'tenant_erasure_schema_drift';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byok-sdk/cloud-dataplane",
3
- "version": "0.11.0",
3
+ "version": "0.13.0",
4
4
  "description": "BYOK SDK hosted data plane: canonical Postgres + R2 stores, migrations, truth transactions, and maintenance",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -49,9 +49,9 @@
49
49
  "clean": "rm -rf dist"
50
50
  },
51
51
  "dependencies": {
52
- "@byok-sdk/cloud": "0.11.0",
53
- "@byok-sdk/core": "0.11.0",
54
- "@byok-sdk/protocol": "0.11.0",
52
+ "@byok-sdk/cloud": "0.13.0",
53
+ "@byok-sdk/core": "0.13.0",
54
+ "@byok-sdk/protocol": "0.13.0",
55
55
  "aws4fetch": "1.0.20",
56
56
  "fast-xml-parser": "^5.10.1",
57
57
  "pg": "^8.22.0"