@byok-sdk/cloud 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.
- package/dist/activity.d.ts +13 -0
- package/dist/agent-home-projections.d.ts +2 -2
- package/dist/agent-reliability.d.ts +9 -0
- package/dist/approval-control.d.ts +75 -0
- package/dist/approval-timeline.d.ts +60 -14
- package/dist/auth/bearer.d.ts +17 -0
- package/dist/auth/plane.d.ts +3 -4
- package/dist/cloud.d.ts +116 -5
- package/dist/composition/in-memory.d.ts +4 -0
- package/dist/coordination.d.ts +8 -0
- package/dist/errors.d.ts +9 -0
- package/dist/handlers/blobs.d.ts +2 -0
- package/dist/handlers/events.d.ts +10 -4
- package/dist/handlers/messages.d.ts +7 -0
- package/dist/inbound.d.ts +45 -5
- package/dist/index.d.ts +12 -8
- package/dist/index.js +1029 -223
- package/dist/index.js.map +1 -1
- package/dist/steer-control.d.ts +81 -0
- package/dist/stores/in-memory/agent-egress.d.ts +1 -1
- package/dist/stores/in-memory/approval-timeline.d.ts +2 -1
- package/dist/stores/in-memory/blobs.d.ts +1 -0
- package/dist/stores/in-memory/dedup.d.ts +8 -5
- package/dist/stores/in-memory/nonces.d.ts +1 -2
- package/dist/stores/in-memory/pairing-codes.d.ts +9 -5
- package/dist/stores/in-memory/task-attempts.d.ts +29 -1
- package/dist/stores/ports.d.ts +177 -29
- package/dist/tenant-stores.d.ts +29 -2
- package/package.json +3 -3
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GAP-2: why a `cloud.steerTask` call was refused, and the one input that
|
|
3
|
+
* decides it.
|
|
4
|
+
*
|
|
5
|
+
* The reference server (`ConnectionHub.steerTask`, `packages/server/src/hub.ts`)
|
|
6
|
+
* reaches this decision from `TaskSnapshot.claimedRuntimeCapabilities` — the
|
|
7
|
+
* capability block the CLAIMING adapter reported for itself on its own
|
|
8
|
+
* `task.claim`, frozen at the `Offered -> Claimed` transition. Cloud keeps the
|
|
9
|
+
* same single source: {@link TaskAttempt.claimedRuntimeCapabilities}, written by
|
|
10
|
+
* the claim that wins the ownership CAS (`stores/ports.ts`) and never again.
|
|
11
|
+
*
|
|
12
|
+
* The gap this closes is not cosmetic: only pi's adapter implements steering,
|
|
13
|
+
* and Claude's and Codex's THROW on receiving `task.steer`, which stalls that
|
|
14
|
+
* device's redelivery cursor at that seq forever. So the refusal has to happen
|
|
15
|
+
* before an envelope exists, from a fact that shares a lifecycle with the
|
|
16
|
+
* task-to-runtime binding.
|
|
17
|
+
*
|
|
18
|
+
* Fail-closed on unknown, deliberately: `steer_unsupported_runtime` covers both
|
|
19
|
+
* "the claiming adapter reported `steer: false`" and "this attempt carries no
|
|
20
|
+
* capability snapshot at all" (a daemon whose claim predates the field, an
|
|
21
|
+
* attempt claimed before migration `0018`). Refusing an unknown is a
|
|
22
|
+
* recoverable, operator-visible error; guessing "supported" reintroduces the
|
|
23
|
+
* permanent cursor stall this gate exists to prevent. There is deliberately NO
|
|
24
|
+
* runtime-id allow-list here either — a table mapping `pi -> steerable` would be
|
|
25
|
+
* a second capability authority in the coordination plane, drifting the moment a
|
|
26
|
+
* runtime gains or loses the feature, and the daemon already reports the truth.
|
|
27
|
+
*
|
|
28
|
+
* SINGLE SOURCE, deliberately: the gate reads the claim snapshot and NOTHING
|
|
29
|
+
* from the connection layer. Cloud's connection-level equivalent is
|
|
30
|
+
* `DeviceRecord.capabilities`, written from a bearer-authenticated `conn.hello`
|
|
31
|
+
* (`inbound.ts`) — discovery data describing a device BUILD, not the adapter
|
|
32
|
+
* that took this task. A device can reconnect later with a different adapter
|
|
33
|
+
* set; a task already running must keep being judged against what was true when
|
|
34
|
+
* it was claimed. Same pin the reference server holds in
|
|
35
|
+
* `packages/server/src/__tests__/steer-runtime-capability-gate.test.ts`.
|
|
36
|
+
*/
|
|
37
|
+
import type { RuntimeId } from '@byok-sdk/protocol';
|
|
38
|
+
import type { TaskAttemptStatus } from './stores/ports';
|
|
39
|
+
/**
|
|
40
|
+
* Stable strings a caller switches on (an operator UI, an HTTP surface mapping
|
|
41
|
+
* this to a status code) rather than matching error text. Byte-identical to the
|
|
42
|
+
* reference server's `SteerRejectionCode` (`packages/server/src/hub.ts`), so a
|
|
43
|
+
* host that moves from the embedded server to the hosted kernel keeps its own
|
|
44
|
+
* mapping unchanged.
|
|
45
|
+
*
|
|
46
|
+
* - `task_terminal` — the attempt already reached `complete`/`failed`/
|
|
47
|
+
* `cancelled`. Checked FIRST, so a terminal attempt that is also (obviously)
|
|
48
|
+
* not running reports the more specific truth, and a steer racing a terminal
|
|
49
|
+
* transition always resolves terminal-first.
|
|
50
|
+
* - `task_not_running` — the attempt exists and is live, but is `offered`/
|
|
51
|
+
* `claimed`/`cancel_requested`; there is no running turn to steer yet.
|
|
52
|
+
* - `steer_unsupported_runtime` — the runtime that CLAIMED this attempt cannot
|
|
53
|
+
* be steered, per the claim-time capability snapshot. A MISSING snapshot
|
|
54
|
+
* rejects under this same code: unknown is not supported.
|
|
55
|
+
*/
|
|
56
|
+
export type SteerRejectionCode = 'steer_unsupported_runtime' | 'task_not_running' | 'task_terminal';
|
|
57
|
+
/**
|
|
58
|
+
* Thrown by `ByokCloud.steerTask` instead of a generic `Error`, so a caller can
|
|
59
|
+
* tell WHY a steer was refused without matching on message text — the same
|
|
60
|
+
* typed-error idiom as `StaleApprovalError` (`approval-control.ts`).
|
|
61
|
+
*
|
|
62
|
+
* A distinct class rather than a `CloudErrorCode`, for the same reason as
|
|
63
|
+
* `StaleApprovalError`: it carries the state and the runtime the caller needs to
|
|
64
|
+
* explain the refusal, and a code alone cannot.
|
|
65
|
+
*
|
|
66
|
+
* Thrown before any mailbox row is allocated — a refused steer is a NON-EVENT,
|
|
67
|
+
* exactly like a refused approval.
|
|
68
|
+
*/
|
|
69
|
+
export declare class SteerRejectedError extends Error {
|
|
70
|
+
readonly taskId: string;
|
|
71
|
+
readonly code: SteerRejectionCode;
|
|
72
|
+
/** The attempt's status at the moment the steer was refused. */
|
|
73
|
+
readonly status: TaskAttemptStatus;
|
|
74
|
+
/** {@link TaskAttempt.claimedRuntime} — `undefined` when nothing was ever recorded, which is itself a reason `steer_unsupported_runtime` can fire. */
|
|
75
|
+
readonly runtime: RuntimeId | undefined;
|
|
76
|
+
constructor(taskId: string, code: SteerRejectionCode,
|
|
77
|
+
/** The attempt's status at the moment the steer was refused. */
|
|
78
|
+
status: TaskAttemptStatus,
|
|
79
|
+
/** {@link TaskAttempt.claimedRuntime} — `undefined` when nothing was ever recorded, which is itself a reason `steer_unsupported_runtime` can fire. */
|
|
80
|
+
runtime: RuntimeId | undefined);
|
|
81
|
+
}
|
|
@@ -8,5 +8,5 @@ export declare class InMemoryAgentEgressStore implements AgentEgressStore {
|
|
|
8
8
|
readonly record: AgentEgressRecord;
|
|
9
9
|
readonly created: boolean;
|
|
10
10
|
}>;
|
|
11
|
-
get(tenant: TenantId, deviceId: string, eventId: string): Promise<AgentEgressRecord | undefined>;
|
|
11
|
+
get(tenant: TenantId, deviceId: string, agentRef: AgentEgressRecord['payload']['agentRef'], eventId: string): Promise<AgentEgressRecord | undefined>;
|
|
12
12
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { Clock, TenantId } from '@byok-sdk/core';
|
|
2
|
-
import { type ApprovalTimelineAppendInput, type ApprovalTimelineStore, type ApprovalTimelineTail } from '../../approval-timeline';
|
|
2
|
+
import { type ApprovalTimelineAppendInput, type ApprovalTimelineResolvePendingInput, type ApprovalTimelineResolvePendingResult, type ApprovalTimelineStore, type ApprovalTimelineTail } from '../../approval-timeline';
|
|
3
3
|
export declare class InMemoryApprovalTimelineStore implements ApprovalTimelineStore {
|
|
4
4
|
#private;
|
|
5
5
|
private readonly clock;
|
|
6
6
|
constructor(clock: Clock);
|
|
7
7
|
append(tenant: TenantId, input: ApprovalTimelineAppendInput): Promise<ApprovalTimelineTail>;
|
|
8
|
+
resolvePending(tenant: TenantId, input: ApprovalTimelineResolvePendingInput): Promise<ApprovalTimelineResolvePendingResult>;
|
|
8
9
|
read(tenant: TenantId, taskId: string): Promise<ApprovalTimelineTail | undefined>;
|
|
9
10
|
}
|
|
@@ -77,6 +77,7 @@ export declare class InMemoryBlobContentProxy implements BlobContentProxy {
|
|
|
77
77
|
#private;
|
|
78
78
|
constructor(registry: InMemoryBlobRegistry);
|
|
79
79
|
verifySignedUrl(blobId: string, action: 'put' | 'get', sig: string, exp: number): Promise<boolean>;
|
|
80
|
+
expectedUploadBytes(blobId: string): Promise<bigint | undefined>;
|
|
80
81
|
writeContent(blobId: string, data: Uint8Array): Promise<BlobWriteResult>;
|
|
81
82
|
/**
|
|
82
83
|
* Never returns `{ok:false}`: this composition holds the bytes in the same
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* In-memory {@link InboundDedupStore} (N3).
|
|
3
3
|
*
|
|
4
|
-
* A bounded ring per (tenant, device)
|
|
5
|
-
* at-least-once (§9), so
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* A bounded ring per physical (tenant, device) or strict (tenant, device,
|
|
5
|
+
* AgentRef) fact, not an unbounded set: the wire is at-least-once (§9), so
|
|
6
|
+
* this makes processing at-most-once without letting a chatty device or Agent
|
|
7
|
+
* grow memory without limit. Check-and-record is one call, so a composition
|
|
8
|
+
* cannot accidentally split it into a racy read-then-write.
|
|
8
9
|
*/
|
|
9
10
|
import { type TenantId } from '@byok-sdk/core';
|
|
11
|
+
import type { AgentRef } from '@byok-sdk/protocol';
|
|
10
12
|
import type { InboundDedupStore } from '../ports';
|
|
11
|
-
/** Ids retained per
|
|
13
|
+
/** Ids retained per physical or Agent-bound key. Same depth as the reference server's ring. */
|
|
12
14
|
export declare const DEDUP_RING_CAPACITY = 1024;
|
|
13
15
|
export declare class InMemoryInboundDedupStore implements InboundDedupStore {
|
|
14
16
|
#private;
|
|
15
17
|
constructor(capacity?: number);
|
|
16
18
|
checkAndRecord(tenant: TenantId, deviceId: string, envelopeId: string): Promise<boolean>;
|
|
19
|
+
checkAndRecordAgent(tenant: TenantId, deviceId: string, agentRef: AgentRef, envelopeId: string): Promise<boolean>;
|
|
17
20
|
}
|
|
@@ -17,6 +17,5 @@ export declare class InMemoryNonceStore implements NonceStore {
|
|
|
17
17
|
/** Number of records currently held (post-sweep). Test-facing only. */
|
|
18
18
|
get size(): number;
|
|
19
19
|
issue(tenant: TenantId, deviceId: string): Promise<string>;
|
|
20
|
-
|
|
21
|
-
markUsed(tenant: TenantId, nonce: string): Promise<void>;
|
|
20
|
+
consumeIfValid(tenant: TenantId, deviceId: string, nonce: string): Promise<boolean>;
|
|
22
21
|
}
|
|
@@ -2,17 +2,21 @@
|
|
|
2
2
|
* In-memory {@link PairingCodeStore}: single-use codes bound to the tenant and
|
|
3
3
|
* product they were minted for.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
5
|
+
* Enrollment answers `undefined` for unknown, expired, and already-used alike.
|
|
6
6
|
* The reference server distinguishes those three in its 401 text; a hosted,
|
|
7
7
|
* multi-tenant surface deliberately does not — the code is a bearer credential
|
|
8
8
|
* addressable across every tenant, and "already used" versus "never existed"
|
|
9
9
|
* is exactly the difference an attacker enumerating codes would pay for.
|
|
10
|
+
*
|
|
11
|
+
* Code issuance and enrollment project this one authority into separate ports.
|
|
12
|
+
* The enrollment path serializes each code through registration and flips
|
|
13
|
+
* `used` only after its shared device directory has accepted the row.
|
|
10
14
|
*/
|
|
11
15
|
import type { Clock, TenantId } from '@byok-sdk/core';
|
|
12
|
-
import type {
|
|
13
|
-
export declare class InMemoryPairingCodeStore implements PairingCodeStore {
|
|
16
|
+
import type { DeviceDirectory, DeviceRecord, PairingEnrollment, PairingEnrollmentInput, PairingCodeInfo, PairingCodeIssueInput, PairingCodeStore } from '../ports';
|
|
17
|
+
export declare class InMemoryPairingCodeStore implements PairingCodeStore, PairingEnrollment {
|
|
14
18
|
#private;
|
|
15
|
-
constructor(clock: Clock);
|
|
19
|
+
constructor(clock: Clock, devices: DeviceDirectory);
|
|
16
20
|
issue(tenant: TenantId, input: PairingCodeIssueInput): Promise<PairingCodeInfo>;
|
|
17
|
-
|
|
21
|
+
redeemAndRegister(input: PairingEnrollmentInput): Promise<DeviceRecord | undefined>;
|
|
18
22
|
}
|
|
@@ -16,7 +16,11 @@
|
|
|
16
16
|
* unfalsifiable.
|
|
17
17
|
*/
|
|
18
18
|
import { type Clock, type TenantId } from '@byok-sdk/core';
|
|
19
|
-
import type {
|
|
19
|
+
import type { RuntimeCapabilities, RuntimeId } from '@byok-sdk/protocol';
|
|
20
|
+
import { type AgentMessageAdmission, type AgentRef, type TaskAttempt, type TaskAttemptListQuery, type TaskAttemptPage, type TaskAttemptStatus, type TaskAttemptStore } from '../ports';
|
|
21
|
+
interface StoredAgentMessageAdmission extends AgentMessageAdmission {
|
|
22
|
+
readonly deviceId: string;
|
|
23
|
+
}
|
|
20
24
|
export declare class InMemoryTaskAttemptStore implements TaskAttemptStore {
|
|
21
25
|
#private;
|
|
22
26
|
constructor(clock: Clock, state?: InMemoryTaskAttemptState);
|
|
@@ -33,11 +37,33 @@ export declare class InMemoryTaskAttemptStore implements TaskAttemptStore {
|
|
|
33
37
|
readonly attempt: TaskAttempt;
|
|
34
38
|
readonly created: boolean;
|
|
35
39
|
}>;
|
|
40
|
+
reserveAgentMessage(tenant: TenantId, input: {
|
|
41
|
+
readonly taskId: string;
|
|
42
|
+
readonly deviceId: string;
|
|
43
|
+
readonly messageId: string;
|
|
44
|
+
readonly payloadBody: string;
|
|
45
|
+
}): Promise<'reserved' | 'pending' | 'rejected'>;
|
|
46
|
+
readAgentMessage(tenant: TenantId, input: {
|
|
47
|
+
readonly taskId: string;
|
|
48
|
+
readonly deviceId: string;
|
|
49
|
+
readonly messageId: string;
|
|
50
|
+
readonly payloadBody: string;
|
|
51
|
+
}): Promise<AgentMessageAdmission | undefined>;
|
|
52
|
+
finalizeAgentMessage(tenant: TenantId, input: {
|
|
53
|
+
readonly taskId: string;
|
|
54
|
+
readonly deviceId: string;
|
|
55
|
+
readonly messageId: string;
|
|
56
|
+
readonly payloadBody: string;
|
|
57
|
+
readonly terminalBody: string;
|
|
58
|
+
}): Promise<AgentMessageAdmission | undefined>;
|
|
36
59
|
get(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
|
|
37
60
|
getMany(tenant: TenantId, taskIds: readonly string[]): Promise<readonly TaskAttempt[]>;
|
|
61
|
+
list(tenant: TenantId, query: TaskAttemptListQuery): Promise<TaskAttemptPage>;
|
|
38
62
|
claim(tenant: TenantId, input: {
|
|
39
63
|
taskId: string;
|
|
40
64
|
deviceId: string;
|
|
65
|
+
runtime?: RuntimeId;
|
|
66
|
+
capabilities?: RuntimeCapabilities;
|
|
41
67
|
}): Promise<TaskAttempt | undefined>;
|
|
42
68
|
recordStatus(tenant: TenantId, input: {
|
|
43
69
|
readonly taskId: string;
|
|
@@ -50,8 +76,10 @@ export declare class InMemoryTaskAttemptStore implements TaskAttemptStore {
|
|
|
50
76
|
export declare class InMemoryTaskAttemptState {
|
|
51
77
|
#private;
|
|
52
78
|
readonly attempts: Map<string, TaskAttempt>;
|
|
79
|
+
readonly messageAdmissions: Map<string, StoredAgentMessageAdmission>;
|
|
53
80
|
constructor(clock: Clock);
|
|
54
81
|
now(): string;
|
|
55
82
|
/** Serialize every state-changing operation for one tenant/task key. */
|
|
56
83
|
mutate<T>(key: string, operation: () => T | Promise<T>): Promise<T>;
|
|
57
84
|
}
|
|
85
|
+
export {};
|
package/dist/stores/ports.d.ts
CHANGED
|
@@ -13,20 +13,18 @@
|
|
|
13
13
|
* 1. **Async.** Every method returns a `Promise`, so a SQL or KV composition
|
|
14
14
|
* can implement the same contract the in-memory reference does.
|
|
15
15
|
* 2. **Tenant-first.** Every method's first parameter is a required
|
|
16
|
-
* `TenantId` — with
|
|
17
|
-
*
|
|
18
|
-
*
|
|
16
|
+
* `TenantId` — with one documented lookup exception below. Pairing code
|
|
17
|
+
* consumption is a separate composition-owned enrollment operation: its
|
|
18
|
+
* bearer code resolves the tenant internally and it returns only the
|
|
19
|
+
* resulting device row.
|
|
19
20
|
*
|
|
20
|
-
* The
|
|
21
|
+
* The lookup exception:
|
|
21
22
|
*
|
|
22
23
|
* - {@link DeviceDirectory.resolveByDeviceId} — `POST /byok/challenge` and
|
|
23
24
|
* `POST /byok/token` carry only a deviceId (the pinned wire contract), and
|
|
24
25
|
* the row is what tells the deployment which tenant to mint for.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* Neither is reachable through {@link TenantStores} (see `tenant-stores.ts`),
|
|
29
|
-
* so a device-facing handler cannot call them.
|
|
26
|
+
* It is not reachable through {@link TenantStores} (see `tenant-stores.ts`),
|
|
27
|
+
* so a device-facing handler cannot call it.
|
|
30
28
|
*
|
|
31
29
|
* The byte-proxy trio that used to be a third pre-tenant exception is no
|
|
32
30
|
* longer part of this bundle at all: it moved to {@link BlobContentProxy},
|
|
@@ -35,7 +33,7 @@
|
|
|
35
33
|
* bytes (see the blobs section below).
|
|
36
34
|
*/
|
|
37
35
|
import type { MailboxBody, MailboxMessage, PresenceStore, StorageReservation, TenantId, TenantReadiness } from '@byok-sdk/core';
|
|
38
|
-
import type { AgentEgressReliablePayload, AgentRef } from '@byok-sdk/protocol';
|
|
36
|
+
import type { AgentEgressReliablePayload, AgentRef, RuntimeCapabilities, RuntimeId } from '@byok-sdk/protocol';
|
|
39
37
|
export type { AgentEgressReliablePayload, AgentRef } from '@byok-sdk/protocol';
|
|
40
38
|
import type { ActivityStore } from '../activity';
|
|
41
39
|
import type { ApprovalTimelineStore } from '../approval-timeline';
|
|
@@ -151,33 +149,61 @@ export interface PairingCodeIssueInput {
|
|
|
151
149
|
}
|
|
152
150
|
export interface PairingCodeStore {
|
|
153
151
|
issue(tenant: TenantId, input: PairingCodeIssueInput): Promise<PairingCodeInfo>;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The pre-tenant facts a pairing request carries into enrollment. Tenant and
|
|
155
|
+
* product are deliberately absent: the guarded pairing-code row is their sole
|
|
156
|
+
* authority, and no caller can choose the registration scope.
|
|
157
|
+
*/
|
|
158
|
+
export interface PairingEnrollmentInput {
|
|
159
|
+
readonly pairingCode: string;
|
|
160
|
+
readonly deviceId: string;
|
|
161
|
+
readonly deviceName: string;
|
|
162
|
+
readonly devicePublicKey: string;
|
|
163
|
+
readonly proofKeyId: string;
|
|
164
|
+
readonly proofKeyEpoch: number;
|
|
165
|
+
readonly machineId?: string;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The only pairing-code consumption operation. Implementations atomically
|
|
169
|
+
* consume a valid code, apply machine supersession/state cleanup, and register
|
|
170
|
+
* the device; failures leave the code retryable. Unknown, expired, and already
|
|
171
|
+
* consumed codes all return `undefined`.
|
|
172
|
+
*/
|
|
173
|
+
export interface PairingEnrollment {
|
|
174
|
+
redeemAndRegister(input: PairingEnrollmentInput): Promise<DeviceRecord | undefined>;
|
|
162
175
|
}
|
|
163
176
|
export interface NonceStore {
|
|
164
177
|
issue(tenant: TenantId, deviceId: string): Promise<string>;
|
|
165
|
-
/**
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
178
|
+
/**
|
|
179
|
+
* Atomically consume a nonce only when it belongs to this exact (tenant,
|
|
180
|
+
* device), is unexpired, and has not already been consumed. Returns `true`
|
|
181
|
+
* for the sole winner; every other case returns `false`.
|
|
182
|
+
*/
|
|
183
|
+
consumeIfValid(tenant: TenantId, deviceId: string, nonce: string): Promise<boolean>;
|
|
169
184
|
}
|
|
170
185
|
export interface InboundDedupStore {
|
|
171
186
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* unbounded set.
|
|
187
|
+
* Device-physical envelope ledger (`conn.hello` and facts with no Agent
|
|
188
|
+
* binding). `true` when `envelopeId` was already seen for this (tenant,
|
|
189
|
+
* device); otherwise records it and returns `false`.
|
|
176
190
|
*/
|
|
177
191
|
checkAndRecord(tenant: TenantId, deviceId: string, envelopeId: string): Promise<boolean>;
|
|
192
|
+
/**
|
|
193
|
+
* Agent-bound envelope ledger. The AgentRef is required: a same-device
|
|
194
|
+
* second Agent must never resolve or suppress this Agent's completed fact.
|
|
195
|
+
* Retention remains bounded per exact (tenant, device, AgentRef).
|
|
196
|
+
*/
|
|
197
|
+
checkAndRecordAgent(tenant: TenantId, deviceId: string, agentRef: AgentRef, envelopeId: string): Promise<boolean>;
|
|
178
198
|
}
|
|
179
199
|
export declare const TASK_ATTEMPT_STATUSES: readonly ['offered', 'claimed', 'running', 'cancel_requested', 'complete', 'failed', 'cancelled'];
|
|
180
200
|
export type TaskAttemptStatus = (typeof TASK_ATTEMPT_STATUSES)[number];
|
|
201
|
+
/** The durable first-message reservation and, once present, its terminal disposition. */
|
|
202
|
+
export interface AgentMessageAdmission {
|
|
203
|
+
readonly messageId: string;
|
|
204
|
+
readonly payloadBody: string;
|
|
205
|
+
readonly terminalBody?: string;
|
|
206
|
+
}
|
|
181
207
|
export interface TaskAttempt {
|
|
182
208
|
readonly tenantId: TenantId;
|
|
183
209
|
readonly taskId: string;
|
|
@@ -188,6 +214,40 @@ export interface TaskAttempt {
|
|
|
188
214
|
/** Set by `task.claim`, and only by the first one. Legacy attempts remain claimable by any tenant device until then; strict Agent attempts are target-device bound before this field is consulted. */
|
|
189
215
|
readonly ownerDeviceId?: string;
|
|
190
216
|
readonly status: TaskAttemptStatus;
|
|
217
|
+
/**
|
|
218
|
+
* The runtime the CLAIMING daemon reported for itself on its own
|
|
219
|
+
* `task.claim` (`TaskClaimPayload.runtime`), snapshotted at the
|
|
220
|
+
* `offered -> claimed` transition and never written again — a redelivered or
|
|
221
|
+
* losing claim leaves it exactly as the winning claim left it.
|
|
222
|
+
*
|
|
223
|
+
* ADR-028 forbids cloud holding EXECUTION state (a live `running`/`thinking`
|
|
224
|
+
* phase, a turn, a PID, the current tool call) — quantities that keep
|
|
225
|
+
* changing while an attempt runs. This is not one: it is written once, at
|
|
226
|
+
* the moment ownership is decided, and is an attribution fact about the
|
|
227
|
+
* attempt. Byte-for-byte the same semantics as the reference server's
|
|
228
|
+
* `TaskSnapshot.claimedRuntime` (`packages/server/src/types.ts`).
|
|
229
|
+
*
|
|
230
|
+
* Absent for a daemon whose `task.claim` carried no `runtime` at all.
|
|
231
|
+
*/
|
|
232
|
+
readonly claimedRuntime?: RuntimeId;
|
|
233
|
+
/**
|
|
234
|
+
* The capability block the CLAIMING adapter reported for ITSELF on that same
|
|
235
|
+
* `task.claim` (`TaskClaimPayload.capabilities`), snapshotted under exactly
|
|
236
|
+
* the same write-once rule as {@link TaskAttempt.claimedRuntime}.
|
|
237
|
+
*
|
|
238
|
+
* This is the ONLY input to the steer gate (`steer-control.ts`,
|
|
239
|
+
* `ByokCloud.steerTask`). The connection-level snapshot cloud keeps —
|
|
240
|
+
* `DeviceRecord.capabilities`, written from `conn.hello`
|
|
241
|
+
* (`inbound.ts`) — describes a device BUILD and never feeds it: a device can
|
|
242
|
+
* reconnect later with a different adapter set, while a task that is already
|
|
243
|
+
* running must keep being judged against what was true when it was claimed.
|
|
244
|
+
* Same single-source rule the reference server pins
|
|
245
|
+
* (`packages/server/src/__tests__/steer-runtime-capability-gate.test.ts`).
|
|
246
|
+
*
|
|
247
|
+
* Absent for a daemon that predates the field, which the gate reads as
|
|
248
|
+
* "unknown" and refuses — unknown is not supported.
|
|
249
|
+
*/
|
|
250
|
+
readonly claimedRuntimeCapabilities?: RuntimeCapabilities;
|
|
191
251
|
/** Runtime-reported terminal cause from the first winning terminal. */
|
|
192
252
|
readonly terminalCause?: string;
|
|
193
253
|
/** Durable host cancellation authority. Its presence outranks later device terminal receipts. */
|
|
@@ -197,6 +257,43 @@ export interface TaskAttempt {
|
|
|
197
257
|
};
|
|
198
258
|
readonly updatedAt: string;
|
|
199
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* One bounded page request over a tenant's task attempts.
|
|
262
|
+
*
|
|
263
|
+
* Keyset-paged by `taskId` ASCENDING, deliberately, and NOT chronologically: an
|
|
264
|
+
* attempt carries no monotonic sequence, and `updatedAt` moves under an
|
|
265
|
+
* in-flight page (a status transition would re-order rows mid-walk and let a
|
|
266
|
+
* caller skip or repeat one). `taskId` is `task_<uuid>` — stable, unique per
|
|
267
|
+
* tenant, and never rewritten — so it is the only column that gives a walk the
|
|
268
|
+
* exactly-once property. The order is therefore arbitrary but TOTAL, which is
|
|
269
|
+
* what a paged read model needs; a caller that wants recency sorts a page
|
|
270
|
+
* itself.
|
|
271
|
+
*
|
|
272
|
+
* Adding a sequence column to get chronological order was rejected: it is a
|
|
273
|
+
* migration across every composition to serve a read-model nicety, and a second
|
|
274
|
+
* ordering authority to keep consistent with the row it decorates.
|
|
275
|
+
*/
|
|
276
|
+
export interface TaskAttemptListQuery {
|
|
277
|
+
/** Maximum attempts in the page. Required and fail-closed: a non-positive or non-integer limit is rejected, never silently defaulted or clamped. */
|
|
278
|
+
readonly limit: number;
|
|
279
|
+
/**
|
|
280
|
+
* The `nextCursor` from the previous page — opaque to the caller, and an
|
|
281
|
+
* EXCLUSIVE lower bound on `taskId`. Absent starts at the beginning. A cursor
|
|
282
|
+
* naming a task that has since been deleted still resolves: it is a bound,
|
|
283
|
+
* not a lookup.
|
|
284
|
+
*/
|
|
285
|
+
readonly cursor?: string;
|
|
286
|
+
}
|
|
287
|
+
export interface TaskAttemptPage {
|
|
288
|
+
readonly attempts: readonly TaskAttempt[];
|
|
289
|
+
/**
|
|
290
|
+
* Pass as the next request's `cursor`. ABSENT means this page is the end of
|
|
291
|
+
* the walk — not "start over" — so a caller stops on absence rather than on
|
|
292
|
+
* an empty page, and a page that exactly fills `limit` with nothing after it
|
|
293
|
+
* still terminates.
|
|
294
|
+
*/
|
|
295
|
+
readonly nextCursor?: string;
|
|
296
|
+
}
|
|
200
297
|
export interface TaskAttemptStore {
|
|
201
298
|
/** Called when an offer is enqueued: records the pending attempt with no owner yet. */
|
|
202
299
|
open(tenant: TenantId, input: {
|
|
@@ -216,13 +313,60 @@ export interface TaskAttemptStore {
|
|
|
216
313
|
readonly attempt: TaskAttempt;
|
|
217
314
|
readonly created: boolean;
|
|
218
315
|
}>;
|
|
316
|
+
/**
|
|
317
|
+
* Atomically binds one message payload to a live task before an external
|
|
318
|
+
* consumer can run. `pending` is an existing exact reservation whose terminal
|
|
319
|
+
* receipt has not been recorded yet; callers retry the same product consumer
|
|
320
|
+
* contract so its own durable idempotency can reconcile a commit whose local
|
|
321
|
+
* finalization failed. Exact pending lookup precedes the live-task gate, while
|
|
322
|
+
* every new reservation remains lifecycle-gated.
|
|
323
|
+
*/
|
|
324
|
+
reserveAgentMessage(tenant: TenantId, input: {
|
|
325
|
+
readonly taskId: string;
|
|
326
|
+
readonly deviceId: string;
|
|
327
|
+
readonly messageId: string;
|
|
328
|
+
readonly payloadBody: string;
|
|
329
|
+
}): Promise<'reserved' | 'pending' | 'rejected'>;
|
|
330
|
+
/** Read only an exact reservation; conflicting task/message bindings are not observable. */
|
|
331
|
+
readAgentMessage(tenant: TenantId, input: {
|
|
332
|
+
readonly taskId: string;
|
|
333
|
+
readonly deviceId: string;
|
|
334
|
+
readonly messageId: string;
|
|
335
|
+
readonly payloadBody: string;
|
|
336
|
+
}): Promise<AgentMessageAdmission | undefined>;
|
|
337
|
+
/** CAS the exact reservation to one immutable terminal disposition body. */
|
|
338
|
+
finalizeAgentMessage(tenant: TenantId, input: {
|
|
339
|
+
readonly taskId: string;
|
|
340
|
+
readonly deviceId: string;
|
|
341
|
+
readonly messageId: string;
|
|
342
|
+
readonly payloadBody: string;
|
|
343
|
+
readonly terminalBody: string;
|
|
344
|
+
}): Promise<AgentMessageAdmission | undefined>;
|
|
219
345
|
get(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
|
|
220
346
|
/** Batch lookup used by mailbox projection; implementations must not turn one poll into N queries. */
|
|
221
347
|
getMany(tenant: TenantId, taskIds: readonly string[]): Promise<readonly TaskAttempt[]>;
|
|
222
|
-
/**
|
|
348
|
+
/**
|
|
349
|
+
* One bounded page of this tenant's attempts, keyset-paged by `taskId` — see
|
|
350
|
+
* {@link TaskAttemptListQuery} for why that key and not a timestamp.
|
|
351
|
+
* Implementations must read one page in one query (`taskId > cursor ORDER BY
|
|
352
|
+
* taskId LIMIT limit + 1`), never the whole tenant filtered in memory.
|
|
353
|
+
*/
|
|
354
|
+
list(tenant: TenantId, query: TaskAttemptListQuery): Promise<TaskAttemptPage>;
|
|
355
|
+
/**
|
|
356
|
+
* First claim wins the ownership; a later claim by the same device is
|
|
357
|
+
* idempotent. No-op (returns `undefined`) for a task this tenant never
|
|
358
|
+
* offered.
|
|
359
|
+
*
|
|
360
|
+
* `runtime`/`capabilities` are the claiming daemon's self-report, written
|
|
361
|
+
* ONLY by the claim that actually wins the ownership CAS — an idempotent
|
|
362
|
+
* re-claim (and a losing one) must leave the recorded snapshot untouched,
|
|
363
|
+
* including when it carries a different or absent value.
|
|
364
|
+
*/
|
|
223
365
|
claim(tenant: TenantId, input: {
|
|
224
366
|
readonly taskId: string;
|
|
225
367
|
readonly deviceId: string;
|
|
368
|
+
readonly runtime?: RuntimeId;
|
|
369
|
+
readonly capabilities?: RuntimeCapabilities;
|
|
226
370
|
}): Promise<TaskAttempt | undefined>;
|
|
227
371
|
/** Record a lifecycle transition. No-op (returns `undefined`) for an unknown task — same shape as the reference server's per-type handlers. */
|
|
228
372
|
recordStatus(tenant: TenantId, input: {
|
|
@@ -287,7 +431,8 @@ export interface AgentEgressStore {
|
|
|
287
431
|
readonly record: AgentEgressRecord;
|
|
288
432
|
readonly created: boolean;
|
|
289
433
|
}>;
|
|
290
|
-
|
|
434
|
+
/** Exact reliable fact lookup; device plus event id alone is ambiguous. */
|
|
435
|
+
get(tenant: TenantId, deviceId: string, agentRef: AgentRef, eventId: string): Promise<AgentEgressRecord | undefined>;
|
|
291
436
|
}
|
|
292
437
|
export interface ProofRequestReceipt {
|
|
293
438
|
readonly tenantId: TenantId;
|
|
@@ -411,6 +556,8 @@ export interface CloudBlobStore {
|
|
|
411
556
|
*/
|
|
412
557
|
export interface BlobContentProxy {
|
|
413
558
|
verifySignedUrl(blobId: string, action: 'put' | 'get', sig: string, exp: number): Promise<boolean>;
|
|
559
|
+
/** Authoritative declared size for a signed PUT; no byte buffering precedes this lookup. */
|
|
560
|
+
expectedUploadBytes(blobId: string): Promise<bigint | undefined>;
|
|
414
561
|
writeContent(blobId: string, data: Uint8Array): Promise<BlobWriteResult>;
|
|
415
562
|
/** `undefined` = no such blob (404); a `{ok:false}` result = the blob exists but its bytes could not be proxied (502, distinguished by {@link BlobReadErrorCode}). */
|
|
416
563
|
readContent(blobId: string): Promise<BlobReadResult | undefined>;
|
|
@@ -436,6 +583,7 @@ export interface CloudStores {
|
|
|
436
583
|
readonly approvals: ApprovalTimelineStore;
|
|
437
584
|
readonly devices: DeviceDirectory;
|
|
438
585
|
readonly pairingCodes: PairingCodeStore;
|
|
586
|
+
readonly pairing: PairingEnrollment;
|
|
439
587
|
readonly nonces: NonceStore;
|
|
440
588
|
readonly dedup: InboundDedupStore;
|
|
441
589
|
readonly tasks: TaskAttemptStore;
|
|
@@ -447,5 +595,5 @@ export interface CloudStores {
|
|
|
447
595
|
readonly rateLimiter: InboundRateLimiter;
|
|
448
596
|
}
|
|
449
597
|
/** Names of every port in {@link CloudStores}, in contract order. */
|
|
450
|
-
export declare const CLOUD_STORE_NAMES: readonly ['activity', 'approvals', 'devices', 'pairingCodes', 'nonces', 'dedup', 'tasks', 'cancellations', 'receipts', 'egress', 'proofReceipts', 'blobs', 'rateLimiter'];
|
|
598
|
+
export declare const CLOUD_STORE_NAMES: readonly ['activity', 'approvals', 'devices', 'pairingCodes', 'pairing', 'nonces', 'dedup', 'tasks', 'cancellations', 'receipts', 'egress', 'proofReceipts', 'blobs', 'rateLimiter'];
|
|
451
599
|
export type CloudStoreName = (typeof CLOUD_STORE_NAMES)[number];
|
package/dist/tenant-stores.d.ts
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
* presigned blob calls) are deliberately absent from this surface: a
|
|
22
22
|
* device-facing handler must not be able to reach them.
|
|
23
23
|
*/
|
|
24
|
-
import { type BoardClaimInput, type BoardItem, type BoardItemInput, type BoardListQuery, type BoardPage, type BoardStatusUpdateInput, type BoardUnclaimInput, type CoreStores, type MailboxAdvanceCursorInput, type MailboxAppendInput, type MailboxCursorState, type MailboxMessage, type MailboxPage, type MailboxReadQuery, type Principal, type PresenceHint, type PresenceHintInput, type TenantReadiness, type StorageFinalizeInput, type StorageFinalizeResult, type StorageReservation, type StorageReservationInput, type TenantId } from '@byok-sdk/core';
|
|
24
|
+
import { type BoardClaimInput, type BoardItem, type BoardItemInput, type BoardListQuery, type BoardPage, type BoardStatusUpdateInput, type BoardUnclaimInput, type CoreStores, type MailboxAdvanceCursorInput, type MailboxAppendInput, type MailboxCursorState, type MailboxMessage, type MailboxPage, type MailboxReadQuery, type MailboxRecordDeliveryInput, type Principal, type PresenceHint, type PresenceHintInput, type TenantReadiness, type StorageFinalizeInput, type StorageFinalizeResult, type StorageReservation, type StorageReservationInput, type TenantId } from '@byok-sdk/core';
|
|
25
|
+
import type { AgentRef, RuntimeCapabilities, RuntimeId } from '@byok-sdk/protocol';
|
|
25
26
|
import type { ActivityAppendInput, ActivityTail } from './activity';
|
|
26
27
|
import type { ApprovalTimelineAppendInput, ApprovalTimelineTail } from './approval-timeline';
|
|
27
28
|
import type { BlobObservation, CloudStores, DeviceRecord, AgentEgressRecord, RequestReceipt, TaskCancellationMutation, TaskCancellationRequest, TaskAttempt, TaskAttemptStatus } from './stores/ports';
|
|
@@ -29,6 +30,7 @@ export interface TenantBoundMailbox {
|
|
|
29
30
|
append(input: MailboxAppendInput): Promise<MailboxMessage>;
|
|
30
31
|
/** Pure read. Never advances the cursor — the daemon's next poll is the only ack. */
|
|
31
32
|
readAfter(query: MailboxReadQuery): Promise<MailboxPage>;
|
|
33
|
+
recordDelivery(input: MailboxRecordDeliveryInput): Promise<MailboxCursorState>;
|
|
32
34
|
advanceCursor(input: MailboxAdvanceCursorInput): Promise<MailboxCursorState>;
|
|
33
35
|
readCursor(deviceId: string): Promise<MailboxCursorState>;
|
|
34
36
|
}
|
|
@@ -77,11 +79,33 @@ export interface TenantBoundTaskAttempts {
|
|
|
77
79
|
readonly attempt: TaskAttempt;
|
|
78
80
|
readonly created: boolean;
|
|
79
81
|
}>;
|
|
82
|
+
reserveAgentMessage(input: {
|
|
83
|
+
readonly taskId: string;
|
|
84
|
+
readonly deviceId: string;
|
|
85
|
+
readonly messageId: string;
|
|
86
|
+
readonly payloadBody: string;
|
|
87
|
+
}): Promise<'reserved' | 'pending' | 'rejected'>;
|
|
88
|
+
readAgentMessage(input: {
|
|
89
|
+
readonly taskId: string;
|
|
90
|
+
readonly deviceId: string;
|
|
91
|
+
readonly messageId: string;
|
|
92
|
+
readonly payloadBody: string;
|
|
93
|
+
}): Promise<import('./stores/ports').AgentMessageAdmission | undefined>;
|
|
94
|
+
finalizeAgentMessage(input: {
|
|
95
|
+
readonly taskId: string;
|
|
96
|
+
readonly deviceId: string;
|
|
97
|
+
readonly messageId: string;
|
|
98
|
+
readonly payloadBody: string;
|
|
99
|
+
readonly terminalBody: string;
|
|
100
|
+
}): Promise<import('./stores/ports').AgentMessageAdmission | undefined>;
|
|
80
101
|
get(taskId: string): Promise<TaskAttempt | undefined>;
|
|
81
102
|
getMany(taskIds: readonly string[]): Promise<readonly TaskAttempt[]>;
|
|
103
|
+
list(query: import('./stores/ports').TaskAttemptListQuery): Promise<import('./stores/ports').TaskAttemptPage>;
|
|
82
104
|
claim(input: {
|
|
83
105
|
readonly taskId: string;
|
|
84
106
|
readonly deviceId: string;
|
|
107
|
+
readonly runtime?: RuntimeId;
|
|
108
|
+
readonly capabilities?: RuntimeCapabilities;
|
|
85
109
|
}): Promise<TaskAttempt | undefined>;
|
|
86
110
|
recordStatus(input: {
|
|
87
111
|
readonly taskId: string;
|
|
@@ -94,7 +118,10 @@ export interface TenantBoundTaskCancellations {
|
|
|
94
118
|
request(input: TaskCancellationRequest): Promise<TaskCancellationMutation | undefined>;
|
|
95
119
|
}
|
|
96
120
|
export interface TenantBoundDedup {
|
|
121
|
+
/** Device-physical envelope ledger. */
|
|
97
122
|
checkAndRecord(deviceId: string, envelopeId: string): Promise<boolean>;
|
|
123
|
+
/** Strict Agent-bound envelope ledger. */
|
|
124
|
+
checkAndRecordAgent(deviceId: string, agentRef: AgentRef, envelopeId: string): Promise<boolean>;
|
|
98
125
|
}
|
|
99
126
|
export interface TenantBoundReceipts {
|
|
100
127
|
record(input: {
|
|
@@ -111,7 +138,7 @@ export interface TenantBoundAgentEgress {
|
|
|
111
138
|
readonly record: AgentEgressRecord;
|
|
112
139
|
readonly created: boolean;
|
|
113
140
|
}>;
|
|
114
|
-
get(deviceId: string, eventId: string): Promise<AgentEgressRecord | undefined>;
|
|
141
|
+
get(deviceId: string, agentRef: AgentRef, eventId: string): Promise<AgentEgressRecord | undefined>;
|
|
115
142
|
}
|
|
116
143
|
export interface TenantBoundBlobs {
|
|
117
144
|
createUpload(reservation: StorageReservation): Promise<{
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@byok-sdk/cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "BYOK SDK hosted device surface: stateless frozen-v1 HTTP handlers over tenant-first @byok-sdk/core ports",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"clean": "rm -rf dist"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@byok-sdk/core": "0.
|
|
48
|
-
"@byok-sdk/protocol": "0.
|
|
47
|
+
"@byok-sdk/core": "0.13.0",
|
|
48
|
+
"@byok-sdk/protocol": "0.13.0",
|
|
49
49
|
"hono": "^4.12.30",
|
|
50
50
|
"zod": "^4.4.3"
|
|
51
51
|
}
|