@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
package/dist/activity.d.ts
CHANGED
|
@@ -49,6 +49,7 @@ export declare const TimelineEventSchema: z.ZodObject<{
|
|
|
49
49
|
sourceEnvelopeId: z.ZodString;
|
|
50
50
|
batchSeq: z.ZodNumber;
|
|
51
51
|
eventIndex: z.ZodNumber;
|
|
52
|
+
sourceDropped: z.ZodOptional<z.ZodNumber>;
|
|
52
53
|
receivedAt: z.ZodISODateTime;
|
|
53
54
|
event: z.ZodUnion<readonly [z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
54
55
|
type: z.ZodLiteral<"progress">;
|
|
@@ -111,11 +112,23 @@ export interface ActivityAppendInput {
|
|
|
111
112
|
readonly capacity?: number;
|
|
112
113
|
}
|
|
113
114
|
export interface ActivityStore {
|
|
115
|
+
/**
|
|
116
|
+
* `sourceEnvelopeId` is the per-envelope idempotency authority. Repeating
|
|
117
|
+
* its exact canonical batch returns the existing tail; changing any batch
|
|
118
|
+
* fact under that identity is rejected rather than appended as a second
|
|
119
|
+
* observation.
|
|
120
|
+
*/
|
|
114
121
|
append(tenant: TenantId, input: ActivityAppendInput): Promise<ActivityTail>;
|
|
115
122
|
read(tenant: TenantId, taskId: string): Promise<ActivityTail | undefined>;
|
|
116
123
|
}
|
|
117
124
|
export declare function validateActivityAppend(input: ActivityAppendInput): number;
|
|
118
125
|
export declare function projectTimelineEvents(input: ActivityAppendInput, receivedAt: string): readonly TimelineEvent[];
|
|
126
|
+
/**
|
|
127
|
+
* Whether one live tail already contains this source envelope's canonical
|
|
128
|
+
* batch. `conflict` includes a partially retained source: its missing events
|
|
129
|
+
* are not evidence that a different batch may reuse the same source identity.
|
|
130
|
+
*/
|
|
131
|
+
export declare function activitySourceBatchState(entries: readonly TimelineEvent[], input: ActivityAppendInput): 'absent' | 'same' | 'conflict';
|
|
119
132
|
export declare function parseTimelineEvents(value: unknown): readonly TimelineEvent[];
|
|
120
133
|
export declare function compareTimelineEvents(left: TimelineEvent, right: TimelineEvent): number;
|
|
121
134
|
export declare function activityCursor(entries: readonly TimelineEvent[]): ActivityCursor | undefined;
|
|
@@ -14,8 +14,8 @@ export interface AgentHomeProjectionReceiptInput {
|
|
|
14
14
|
readonly agentRef: AgentHomeProjectionPayload['agentRef'];
|
|
15
15
|
readonly projectionHash: AgentHomeProjectionPayload['projectionHash'];
|
|
16
16
|
}
|
|
17
|
-
export declare function agentHomeProjectionRequestKey(deviceId: string, requestId: string): string;
|
|
18
|
-
export declare function agentHomeProjectionCompletionKey(deviceId: string, requestId: string): string;
|
|
17
|
+
export declare function agentHomeProjectionRequestKey(deviceId: string, agentRef: AgentHomeProjectionPayload['agentRef'], requestId: string): string;
|
|
18
|
+
export declare function agentHomeProjectionCompletionKey(deviceId: string, agentRef: AgentHomeProjectionPayload['agentRef'], requestId: string): string;
|
|
19
19
|
export declare function sameAgentHomeProjectionRequest(expected: AgentHomeProjectionPayload, actual: AgentHomeProjectionPayload): boolean;
|
|
20
20
|
export declare function receiptMatchesAgentHomeProjection(request: AgentHomeProjectionPayload, receipt: AgentHomeProjectionCompletionRequest): boolean;
|
|
21
21
|
export declare function statusInputMatchesAgentHomeProjection(request: AgentHomeProjectionPayload, input: AgentHomeProjectionReceiptInput): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type AgentRef } from '@byok-sdk/protocol';
|
|
2
|
+
/**
|
|
3
|
+
* Exact, unambiguous storage identity for one Agent-scoped reliability fact.
|
|
4
|
+
*
|
|
5
|
+
* The device remains explicit because one Agent is placed on at most one
|
|
6
|
+
* device today, while one device may run many Agents. JSON supplies a stable
|
|
7
|
+
* length-delimited encoding without inventing a Placement authority.
|
|
8
|
+
*/
|
|
9
|
+
export declare function agentReliabilityKey(domain: string, deviceId: string, agentRef: AgentRef, id: string): string;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host-side approval resolution: the one rule that decides WHICH approval a
|
|
3
|
+
* `cloud.approveTask`/`rejectTask` call is allowed to resolve.
|
|
4
|
+
*
|
|
5
|
+
* `@byok-sdk/server` keeps this as a single mutable slot on its task record
|
|
6
|
+
* (`TaskSnapshot.pendingApprovalId`, `packages/server/src/hub.ts`) that
|
|
7
|
+
* `task.await_approval` overwrites and `task.approval_resolved` clears. Cloud
|
|
8
|
+
* holds no session state and no second store: the SAME two observations are
|
|
9
|
+
* already durable in the `ApprovalTimelineStore` tail
|
|
10
|
+
* (`approval-timeline.ts`), appended by the inbound gate, so the slot is
|
|
11
|
+
* DERIVED from that tail on each call rather than mirrored into a record that
|
|
12
|
+
* could then disagree with it.
|
|
13
|
+
*
|
|
14
|
+
* The derivation is a fold over the tail's entries in revision order, holding
|
|
15
|
+
* exactly one slot — deliberately the server's shape, not a set of concurrently
|
|
16
|
+
* pending approvals:
|
|
17
|
+
*
|
|
18
|
+
* - `approval_requested` SETS the slot, superseding whatever it held. A
|
|
19
|
+
* daemon that dispatched a fresh approval without this cloud ever seeing
|
|
20
|
+
* the previous one resolved has moved on, and the newest request is what
|
|
21
|
+
* an operator is being asked about (`hub.ts:1399`).
|
|
22
|
+
* - `approval_resolved(X)` CLEARS the slot when the slot's own id is `X`, or
|
|
23
|
+
* when the slot has no id at all (a pre-M5 daemon never reported one, so
|
|
24
|
+
* the resolution can only be about the single outstanding request). A
|
|
25
|
+
* resolution naming some OTHER id is about an approval already superseded
|
|
26
|
+
* and leaves the current slot standing (`hub.ts:1569`).
|
|
27
|
+
*
|
|
28
|
+
* Two ways there is legitimately nothing pending: the tail has no unresolved
|
|
29
|
+
* request, or the tail is gone entirely — it is a bounded, TTL'd observation
|
|
30
|
+
* ring, not an authority that keeps facts forever. Both answer `undefined`,
|
|
31
|
+
* and the callers fail closed on it. A dropped-then-expired approval is not
|
|
32
|
+
* silently approved.
|
|
33
|
+
*/
|
|
34
|
+
import type { ApprovalTimelineTail } from './approval-timeline';
|
|
35
|
+
/**
|
|
36
|
+
* M5 (approval targeting): thrown by `ByokCloud.approveTask`/`rejectTask` when
|
|
37
|
+
* the caller supplied an `approvalId` that does NOT match the approval this
|
|
38
|
+
* task currently has pending — the caller is targeting a SPECIFIC approval the
|
|
39
|
+
* daemon has already superseded with a newer one.
|
|
40
|
+
*
|
|
41
|
+
* Distinct from the `task_not_awaiting_approval` `ByokCloudError` (there
|
|
42
|
+
* is no pending approval at all, checked FIRST so it still wins when both would
|
|
43
|
+
* apply): this error means the task genuinely IS awaiting an approval, just not
|
|
44
|
+
* the one the caller thinks it is. Thrown before any mailbox row is allocated —
|
|
45
|
+
* a stale-id call has zero side effects.
|
|
46
|
+
*
|
|
47
|
+
* A distinct class rather than a `CloudErrorCode`, unlike the rest of
|
|
48
|
+
* this package's failures, because it carries the two ids the caller needs to
|
|
49
|
+
* re-target: a code alone cannot say what the pending approval actually is.
|
|
50
|
+
*/
|
|
51
|
+
export declare class StaleApprovalError extends Error {
|
|
52
|
+
readonly taskId: string;
|
|
53
|
+
readonly requestedApprovalId: string;
|
|
54
|
+
readonly currentApprovalId: string | undefined;
|
|
55
|
+
constructor(taskId: string, requestedApprovalId: string, currentApprovalId: string | undefined);
|
|
56
|
+
}
|
|
57
|
+
/** The one approval a task currently has outstanding. */
|
|
58
|
+
export interface PendingApproval {
|
|
59
|
+
/**
|
|
60
|
+
* The daemon's own id for this approval. ABSENT for a pre-M5 daemon that
|
|
61
|
+
* reported `task.await_approval` without one — the approval is still pending
|
|
62
|
+
* and still resolvable, it just cannot be targeted, so a caller-supplied
|
|
63
|
+
* `approvalId` proceeds untargeted exactly as it does on the server.
|
|
64
|
+
*/
|
|
65
|
+
readonly approvalId?: string;
|
|
66
|
+
/** Immutable daemon envelope identity for this exact request observation. */
|
|
67
|
+
readonly sourceEnvelopeId: string;
|
|
68
|
+
/** Monotonic timeline revision for this exact request observation. */
|
|
69
|
+
readonly revision: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The task's current pending approval, or `undefined` when it has none — see
|
|
73
|
+
* this module's own doc comment for the fold and why it holds one slot.
|
|
74
|
+
*/
|
|
75
|
+
export declare function pendingApproval(tail: ApprovalTimelineTail | undefined): PendingApproval | undefined;
|
|
@@ -3,44 +3,66 @@ import { z } from 'zod';
|
|
|
3
3
|
export declare const DEFAULT_APPROVAL_TIMELINE_CAPACITY = 50;
|
|
4
4
|
export declare const DEFAULT_APPROVAL_TIMELINE_TTL_MS: number;
|
|
5
5
|
export declare const APPROVAL_SUMMARY_MAX_BYTES: number;
|
|
6
|
-
export declare const ApprovalTimelineEventSchema: z.
|
|
6
|
+
export declare const ApprovalTimelineEventSchema: z.ZodUnion<readonly [z.ZodObject<{
|
|
7
7
|
type: z.ZodLiteral<"approval_requested">;
|
|
8
8
|
summary: z.ZodString;
|
|
9
9
|
approvalId: z.ZodOptional<z.ZodString>;
|
|
10
|
-
}, z.core.$
|
|
10
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
11
11
|
type: z.ZodLiteral<"approval_resolved">;
|
|
12
|
-
approvalId: z.ZodString
|
|
12
|
+
approvalId: z.ZodOptional<z.ZodString>;
|
|
13
13
|
decision: z.ZodEnum<{
|
|
14
14
|
approve: "approve";
|
|
15
15
|
reject: "reject";
|
|
16
16
|
}>;
|
|
17
|
-
resolvedBy: z.
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
resolvedBy: z.ZodLiteral<"local">;
|
|
18
|
+
at: z.ZodISODateTime;
|
|
19
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
20
|
+
type: z.ZodLiteral<"approval_resolved">;
|
|
21
|
+
approvalId: z.ZodOptional<z.ZodString>;
|
|
22
|
+
decision: z.ZodLiteral<"approve">;
|
|
23
|
+
resolvedBy: z.ZodLiteral<"host">;
|
|
20
24
|
at: z.ZodISODateTime;
|
|
21
|
-
}, z.core.$
|
|
25
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
26
|
+
type: z.ZodLiteral<"approval_resolved">;
|
|
27
|
+
approvalId: z.ZodOptional<z.ZodString>;
|
|
28
|
+
decision: z.ZodLiteral<"reject">;
|
|
29
|
+
resolvedBy: z.ZodLiteral<"host">;
|
|
30
|
+
reason: z.ZodNullable<z.ZodString>;
|
|
31
|
+
at: z.ZodISODateTime;
|
|
32
|
+
}, z.core.$strict>]>;
|
|
22
33
|
export type ApprovalTimelineEvent = Readonly<z.infer<typeof ApprovalTimelineEventSchema>>;
|
|
23
34
|
export declare const ApprovalObservationSchema: z.ZodObject<{
|
|
24
35
|
taskId: z.ZodString;
|
|
25
36
|
sourceEnvelopeId: z.ZodString;
|
|
26
37
|
revision: z.ZodNumber;
|
|
27
38
|
receivedAt: z.ZodISODateTime;
|
|
28
|
-
event: z.
|
|
39
|
+
event: z.ZodUnion<readonly [z.ZodObject<{
|
|
29
40
|
type: z.ZodLiteral<"approval_requested">;
|
|
30
41
|
summary: z.ZodString;
|
|
31
42
|
approvalId: z.ZodOptional<z.ZodString>;
|
|
32
|
-
}, z.core.$
|
|
43
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
33
44
|
type: z.ZodLiteral<"approval_resolved">;
|
|
34
|
-
approvalId: z.ZodString
|
|
45
|
+
approvalId: z.ZodOptional<z.ZodString>;
|
|
35
46
|
decision: z.ZodEnum<{
|
|
36
47
|
approve: "approve";
|
|
37
48
|
reject: "reject";
|
|
38
49
|
}>;
|
|
39
|
-
resolvedBy: z.
|
|
40
|
-
|
|
41
|
-
|
|
50
|
+
resolvedBy: z.ZodLiteral<"local">;
|
|
51
|
+
at: z.ZodISODateTime;
|
|
52
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
53
|
+
type: z.ZodLiteral<"approval_resolved">;
|
|
54
|
+
approvalId: z.ZodOptional<z.ZodString>;
|
|
55
|
+
decision: z.ZodLiteral<"approve">;
|
|
56
|
+
resolvedBy: z.ZodLiteral<"host">;
|
|
42
57
|
at: z.ZodISODateTime;
|
|
43
|
-
}, z.core.$
|
|
58
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
59
|
+
type: z.ZodLiteral<"approval_resolved">;
|
|
60
|
+
approvalId: z.ZodOptional<z.ZodString>;
|
|
61
|
+
decision: z.ZodLiteral<"reject">;
|
|
62
|
+
resolvedBy: z.ZodLiteral<"host">;
|
|
63
|
+
reason: z.ZodNullable<z.ZodString>;
|
|
64
|
+
at: z.ZodISODateTime;
|
|
65
|
+
}, z.core.$strict>]>;
|
|
44
66
|
}, z.core.$strip>;
|
|
45
67
|
export type ApprovalObservation = Readonly<z.infer<typeof ApprovalObservationSchema>>;
|
|
46
68
|
export interface ApprovalTimelineTail {
|
|
@@ -59,8 +81,31 @@ export interface ApprovalTimelineAppendInput {
|
|
|
59
81
|
readonly ttlMs?: number;
|
|
60
82
|
readonly capacity?: number;
|
|
61
83
|
}
|
|
84
|
+
/**
|
|
85
|
+
* One host decision against the exact unresolved request it observed.
|
|
86
|
+
*
|
|
87
|
+
* The expected source/revision pair is the durable request identity. A
|
|
88
|
+
* resolver must not turn a stale read into a decision for a request which has
|
|
89
|
+
* since been superseded, and it must not split one logical decision into
|
|
90
|
+
* several mailbox controls. Implementations serialize this comparison and
|
|
91
|
+
* append under the timeline's per-task authority.
|
|
92
|
+
*/
|
|
93
|
+
export interface ApprovalTimelineResolvePendingInput extends ApprovalTimelineAppendInput {
|
|
94
|
+
readonly expectedSourceEnvelopeId: string;
|
|
95
|
+
readonly expectedRevision: number;
|
|
96
|
+
}
|
|
97
|
+
/** Result of a conditional host-decision append. Logical conflicts are data, not transport failures. */
|
|
98
|
+
export type ApprovalTimelineResolvePendingResult = {
|
|
99
|
+
readonly status: 'applied' | 'replayed';
|
|
100
|
+
readonly tail: ApprovalTimelineTail;
|
|
101
|
+
} | {
|
|
102
|
+
readonly status: 'conflict' | 'superseded' | 'absent';
|
|
103
|
+
readonly tail?: ApprovalTimelineTail;
|
|
104
|
+
};
|
|
62
105
|
export interface ApprovalTimelineStore {
|
|
63
106
|
append(tenant: TenantId, input: ApprovalTimelineAppendInput): Promise<ApprovalTimelineTail>;
|
|
107
|
+
/** Atomically append one host resolution only if its request remains current. */
|
|
108
|
+
resolvePending(tenant: TenantId, input: ApprovalTimelineResolvePendingInput): Promise<ApprovalTimelineResolvePendingResult>;
|
|
64
109
|
read(tenant: TenantId, taskId: string): Promise<ApprovalTimelineTail | undefined>;
|
|
65
110
|
}
|
|
66
111
|
export interface ValidatedApprovalTimelineAppend {
|
|
@@ -69,6 +114,7 @@ export interface ValidatedApprovalTimelineAppend {
|
|
|
69
114
|
readonly event: ApprovalTimelineEvent;
|
|
70
115
|
}
|
|
71
116
|
export declare function validateApprovalTimelineAppend(input: ApprovalTimelineAppendInput): ValidatedApprovalTimelineAppend;
|
|
117
|
+
export declare function validateApprovalTimelineResolve(input: ApprovalTimelineResolvePendingInput): ValidatedApprovalTimelineAppend;
|
|
72
118
|
export declare function approvalTimelineKey(tenant: TenantId, taskId: string): string;
|
|
73
119
|
export declare function parseApprovalObservations(value: unknown): readonly ApprovalObservation[];
|
|
74
120
|
export declare function approvalTimelineCursor(entries: readonly ApprovalObservation[]): number | undefined;
|
package/dist/auth/bearer.d.ts
CHANGED
|
@@ -20,6 +20,23 @@ import type { TokenSigner } from './tokens';
|
|
|
20
20
|
export interface BearerAuthDeps {
|
|
21
21
|
readonly tokenSigner: TokenSigner;
|
|
22
22
|
readonly devices: DeviceDirectory;
|
|
23
|
+
/**
|
|
24
|
+
* The ONE product this deployment serves, when it serves exactly one.
|
|
25
|
+
*
|
|
26
|
+
* Two deployment shapes, two explicit authorities — not a default and a
|
|
27
|
+
* fallback. A multi-product hosted control plane leaves this absent, and the
|
|
28
|
+
* row is the whole product authority (`device.productId === claims.productId`
|
|
29
|
+
* below). A single-product instance supplies it — that is the embedded shape,
|
|
30
|
+
* where `createByokServer({ productId })` always has one — and the token must
|
|
31
|
+
* then name THAT product as well, so a device paired through a code minted
|
|
32
|
+
* for another product cannot reach this instance's bearer-authed routes at
|
|
33
|
+
* all.
|
|
34
|
+
*
|
|
35
|
+
* Absent it, a cross-product device whose token and row agree with each other
|
|
36
|
+
* authenticates on every `device` route the deployment mounts, which is
|
|
37
|
+
* precisely the drift this closes for the embedded shape.
|
|
38
|
+
*/
|
|
39
|
+
readonly instanceProductId?: string;
|
|
23
40
|
}
|
|
24
41
|
export declare function extractBearerToken(header: string | undefined): string | undefined;
|
|
25
42
|
export declare function authenticateBearer(header: string | undefined, deps: BearerAuthDeps): Promise<DevicePrincipal | undefined>;
|
package/dist/auth/plane.d.ts
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* claims never sit in handler scope as a tenant a handler could substitute;
|
|
14
14
|
* - a device resolves to a row (the pre-tenant lookup, §6.2's pinned wire
|
|
15
15
|
* contract), and unknown/revoked collapse to one answer;
|
|
16
|
-
* - nonce issue/
|
|
16
|
+
* - nonce issue/atomic-consume and token minting take that ROW, so the
|
|
17
17
|
* tenant they act on is the row's, by construction.
|
|
18
18
|
*/
|
|
19
19
|
import type { Clock, TenantId } from '@byok-sdk/core';
|
|
@@ -52,9 +52,8 @@ export interface AuthPlane {
|
|
|
52
52
|
/** The pre-tenant device lookup §6.2's wire contract forces. `undefined` for unknown AND revoked alike — no existence oracle. */
|
|
53
53
|
resolveDevice(deviceId: string): Promise<DeviceRecord | undefined>;
|
|
54
54
|
issueNonce(device: DeviceRecord): Promise<string>;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
consumeNonce(device: DeviceRecord, nonce: string): Promise<void>;
|
|
55
|
+
/** Atomically consume the nonce after signature verification (§6.2). */
|
|
56
|
+
consumeNonceIfValid(device: DeviceRecord, nonce: string): Promise<boolean>;
|
|
58
57
|
/** Domain-separated (`byok-nonce-v1\n`) — see `verify.ts`. A raw signature over the bare nonce is invalid, with no second accepted form. */
|
|
59
58
|
verifySignature(device: DeviceRecord, nonce: string, signature: string): Promise<boolean>;
|
|
60
59
|
mintAccessToken(device: DeviceRecord): Promise<MintedAccessToken>;
|
package/dist/cloud.d.ts
CHANGED
|
@@ -17,13 +17,14 @@
|
|
|
17
17
|
import { type BoardItem, type BoardItemInput, type BoardListQuery, type BoardPage, type CapabilityDeclaration, type Clock, type CoreStores, type PresenceHint, type SkillPackStore, type TenantId, type TenantReadiness } from '@byok-sdk/core';
|
|
18
18
|
import type { ActivityTail } from './activity';
|
|
19
19
|
import type { ApprovalTimelineTail } from './approval-timeline';
|
|
20
|
-
import { type Envelope, type AgentContentReadPayload, type AgentMessagePublishPayload, type AgentMessageServerContext, type AgentHomeProjectionCompletionRequest, type AgentHomeProjectionPayload, type AgentHomeProjectionReadback, type AgentMemoryProjectionEraseResult, type TaskOfferPayload, type TaskOfferForAgentPayload, type TaskOfferForAgentWithEgressPayload, type TaskOfferForAgentWithEgressFreshPayload, type TaskOfferWithToolsetsPayload } from '@byok-sdk/protocol';
|
|
20
|
+
import { type Envelope, type AgentRef, type AgentContentReadPayload, type AgentMessagePublishPayload, type AgentMessageServerContext, type AgentHomeProjectionCompletionRequest, type AgentHomeProjectionPayload, type AgentHomeProjectionReadback, type AgentMemoryProjectionEraseResult, type TaskOfferPayload, type TaskSteerPayload, type TaskOfferForAgentPayload, type TaskOfferForAgentWithEgressPayload, type TaskOfferForAgentWithEgressFreshPayload, type TaskOfferWithToolsetsPayload } from '@byok-sdk/protocol';
|
|
21
21
|
import type { TokenSigner } from './auth/tokens';
|
|
22
22
|
import type { CloudCrypto } from './crypto/port';
|
|
23
23
|
import { type RouteDescriptor } from './router/registry';
|
|
24
|
+
import { type ByokCloudObserver } from './inbound';
|
|
24
25
|
import { type AgentHomeProjectionReceiptInput } from './agent-home-projections';
|
|
25
26
|
import type { AgentMemoryProjectionAuthorizer, AgentMemoryProjectionStore } from './agent-memory-projection';
|
|
26
|
-
import type { BlobContentProxy, CloudStores, DeviceRecord, AgentEgressRecord, PairingCodeInfo, RequestReceipt, TaskAttempt } from './stores/ports';
|
|
27
|
+
import type { BlobContentProxy, CloudStores, DeviceRecord, AgentEgressRecord, PairingCodeInfo, RequestReceipt, TaskAttempt, TaskAttemptListQuery, TaskAttemptPage } from './stores/ports';
|
|
27
28
|
import { type TerminalResult } from './terminal-result';
|
|
28
29
|
import type { TruthCommitter, TruthObjectDownloads } from './truth/contract';
|
|
29
30
|
/** Matches the reference server's ceiling (§7). */
|
|
@@ -67,7 +68,24 @@ export interface ByokCloudOptions {
|
|
|
67
68
|
readonly capabilities: CapabilityDeclaration;
|
|
68
69
|
/** Recorded on the control-plane principal every host-side call is made under. */
|
|
69
70
|
readonly operatorId?: string;
|
|
70
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* The ONE product this deployment serves, when it serves exactly one — the
|
|
73
|
+
* embedded shape. Supplying it makes every bearer-authed route additionally
|
|
74
|
+
* require the token to name this product; omitting it leaves the device row
|
|
75
|
+
* as the whole product authority. Two deployment shapes, two explicit
|
|
76
|
+
* authorities: see `BearerAuthDeps.instanceProductId` (`auth/bearer.ts`).
|
|
77
|
+
*/
|
|
78
|
+
readonly instanceProductId?: string;
|
|
79
|
+
/**
|
|
80
|
+
* Product-owned consumer; destination lookup uses authenticated task context.
|
|
81
|
+
* Delivery is at least once, including concurrent calls and retries after a
|
|
82
|
+
* process restart or an uncertain commit. The product must durably deduplicate
|
|
83
|
+
* the exact tenant/device/task/AgentRef/message identity and payload, producing
|
|
84
|
+
* one logical effect and the same outcome/reasonCode on every exact replay.
|
|
85
|
+
* A thrown error leaves admission pending for transport retry; only a returned
|
|
86
|
+
* outcome is a terminal product decision. Do not use callback invocation count
|
|
87
|
+
* as execution authority.
|
|
88
|
+
*/
|
|
71
89
|
readonly agentMessage?: {
|
|
72
90
|
consume(input: {
|
|
73
91
|
readonly tenant: TenantId;
|
|
@@ -80,6 +98,14 @@ export interface ByokCloudOptions {
|
|
|
80
98
|
readonly reasonCode?: string;
|
|
81
99
|
}>;
|
|
82
100
|
};
|
|
101
|
+
/**
|
|
102
|
+
* Post-commit relay for envelopes this cloud durably accepted. Read-only by
|
|
103
|
+
* construction: it is told after the write, it returns `void`, and a throw
|
|
104
|
+
* from it is swallowed — the outcome is already fixed. Distinct from
|
|
105
|
+
* {@link ByokCloudOptions.agentMessage}, which is admission and runs BEFORE
|
|
106
|
+
* a write, deciding whether it happens at all.
|
|
107
|
+
*/
|
|
108
|
+
readonly observer?: ByokCloudObserver;
|
|
83
109
|
/** Embedder-owned grant and consent authority for optional hosted Agent-memory projection. */
|
|
84
110
|
readonly agentMemoryProjectionAuthorizer?: AgentMemoryProjectionAuthorizer;
|
|
85
111
|
/** Durable snapshot + immutable metering receipt authority for optional hosted Agent-memory projection. */
|
|
@@ -152,6 +178,20 @@ export interface AgentContentReadInput {
|
|
|
152
178
|
export type AgentHomeProjectionInput = AgentHomeProjectionPayload;
|
|
153
179
|
/** Exact request identity a host must echo to read back durable projection status. */
|
|
154
180
|
export type AgentHomeProjectionStatusInput = AgentHomeProjectionReceiptInput;
|
|
181
|
+
/** Optional targeting for {@link ByokCloud.approveTask}. */
|
|
182
|
+
export interface ApproveTaskOptions {
|
|
183
|
+
/**
|
|
184
|
+
* Resolve THIS specific approval rather than whichever one is pending.
|
|
185
|
+
* A mismatch against the task's current pending approval throws
|
|
186
|
+
* {@link StaleApprovalError} instead of resolving the wrong thing.
|
|
187
|
+
*/
|
|
188
|
+
readonly approvalId?: string;
|
|
189
|
+
}
|
|
190
|
+
/** Optional targeting and refusal cause for {@link ByokCloud.rejectTask}. */
|
|
191
|
+
export interface RejectTaskOptions extends ApproveTaskOptions {
|
|
192
|
+
/** Carried to the runtime verbatim; absent stays absent on the wire. */
|
|
193
|
+
readonly reason?: string;
|
|
194
|
+
}
|
|
155
195
|
export interface EnqueuedAgentControl {
|
|
156
196
|
readonly seq: number;
|
|
157
197
|
readonly envelope: Envelope;
|
|
@@ -223,11 +263,82 @@ export interface ByokCloud {
|
|
|
223
263
|
eraseAgentMemoryProjection(tenant: TenantId, agentId: string): Promise<AgentMemoryProjectionEraseResult>;
|
|
224
264
|
/** Host control plane: durably request cancellation by tenant/task id. Idempotent. */
|
|
225
265
|
cancelTask(tenant: TenantId, taskId: string, reason?: string): Promise<TaskAttempt>;
|
|
266
|
+
/**
|
|
267
|
+
* Host control plane: resolve the approval a paused task is waiting on, by
|
|
268
|
+
* enqueueing `task.approve` to the device that claimed it.
|
|
269
|
+
*
|
|
270
|
+
* WHICH approval is a derived fact, not a stored one — see
|
|
271
|
+
* `approval-control.ts` for the fold over the durable timeline that produces
|
|
272
|
+
* it, and why cloud adds no second authority for it. Gate order mirrors the
|
|
273
|
+
* reference server (`packages/server/src/hub.ts`), and every gate is
|
|
274
|
+
* evaluated in full before a mailbox row is allocated, so a refused call has
|
|
275
|
+
* zero side effects:
|
|
276
|
+
*
|
|
277
|
+
* 1. no such task for this tenant -> `task_not_found`;
|
|
278
|
+
* 2. nothing to resolve -> `task_not_awaiting_approval`, which covers a
|
|
279
|
+
* terminal attempt, an unclaimed one, and a timeline with no unresolved
|
|
280
|
+
* request (expired or never written). Checked BEFORE targeting, so it
|
|
281
|
+
* still wins when both would apply;
|
|
282
|
+
* 3. `opts.approvalId` names an approval this task has already superseded
|
|
283
|
+
* -> {@link StaleApprovalError}. When the daemon never reported an id
|
|
284
|
+
* at all, the call proceeds untargeted, exactly as on the server.
|
|
285
|
+
*
|
|
286
|
+
* Delivery is a best-effort notification, the same contract the wire message
|
|
287
|
+
* has always had (`TaskApprovePayloadSchema`, `@byok-sdk/protocol`): the
|
|
288
|
+
* enqueued envelope is durable, the runtime's response to it is observable
|
|
289
|
+
* only through that task's own later messages. The outgoing `approvalId` is
|
|
290
|
+
* the caller's when given, else the pending one, else omitted.
|
|
291
|
+
*/
|
|
292
|
+
approveTask(tenant: TenantId, taskId: string, opts?: ApproveTaskOptions): Promise<EnqueuedAgentControl>;
|
|
293
|
+
/**
|
|
294
|
+
* Host control plane: refuse the approval a paused task is waiting on, by
|
|
295
|
+
* enqueueing `task.reject` to the device that claimed it. Same gates, same
|
|
296
|
+
* targeting, and the same best-effort delivery contract as
|
|
297
|
+
* {@link ByokCloud.approveTask} — `opts.reason` is carried to the runtime
|
|
298
|
+
* verbatim and is never synthesized when absent.
|
|
299
|
+
*/
|
|
300
|
+
rejectTask(tenant: TenantId, taskId: string, opts?: RejectTaskOptions): Promise<EnqueuedAgentControl>;
|
|
301
|
+
/**
|
|
302
|
+
* Host control plane: inject steering text into a RUNNING task, by enqueueing
|
|
303
|
+
* `task.steer` to the device that claimed it.
|
|
304
|
+
*
|
|
305
|
+
* Gated on the claim-time capability snapshot and on nothing else — see
|
|
306
|
+
* `steer-control.ts` for why that is the only admissible input and why an
|
|
307
|
+
* absent snapshot refuses. Gate order mirrors the reference server
|
|
308
|
+
* (`ConnectionHub.steerTask`, `packages/server/src/hub.ts`), and every gate is
|
|
309
|
+
* evaluated in full before a mailbox row is allocated, so a refused call has
|
|
310
|
+
* zero side effects:
|
|
311
|
+
*
|
|
312
|
+
* 1. no such task for this tenant -> `task_not_found` ({@link ByokCloudError});
|
|
313
|
+
* 2. terminal attempt -> {@link SteerRejectedError} `task_terminal`,
|
|
314
|
+
* checked BEFORE the running check so a steer racing a terminal
|
|
315
|
+
* transition always resolves terminal-first;
|
|
316
|
+
* 3. not running (or running with no owning device) ->
|
|
317
|
+
* {@link SteerRejectedError} `task_not_running`;
|
|
318
|
+
* 4. the claim snapshot does not positively say `steer: true` ->
|
|
319
|
+
* {@link SteerRejectedError} `steer_unsupported_runtime`, including when
|
|
320
|
+
* there is no snapshot at all.
|
|
321
|
+
*
|
|
322
|
+
* Delivery is the same best-effort notification every control message has:
|
|
323
|
+
* the enqueued envelope is durable, and whether the runtime acted on it is
|
|
324
|
+
* observable only through that task's own later messages.
|
|
325
|
+
*/
|
|
326
|
+
steerTask(tenant: TenantId, taskId: string, payload: TaskSteerPayload): Promise<EnqueuedAgentControl>;
|
|
226
327
|
readTaskAttempt(tenant: TenantId, taskId: string): Promise<TaskAttempt | undefined>;
|
|
328
|
+
/**
|
|
329
|
+
* Host control plane: one bounded page of this tenant's task attempts,
|
|
330
|
+
* keyset-paged by `taskId` — see {@link TaskAttemptListQuery} for why the key
|
|
331
|
+
* is the task id and not a timestamp, and why the walk terminates on an
|
|
332
|
+
* absent `nextCursor` rather than on an empty page.
|
|
333
|
+
*
|
|
334
|
+
* A non-positive or non-integer `limit` is rejected
|
|
335
|
+
* (`coordination_input_invalid`), never defaulted: the host names the bound.
|
|
336
|
+
*/
|
|
337
|
+
listTaskAttempts(tenant: TenantId, query: TaskAttemptListQuery): Promise<TaskAttemptPage>;
|
|
227
338
|
/** 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). */
|
|
228
339
|
readTerminalReceipt(tenant: TenantId, taskId: string): Promise<RequestReceipt | undefined>;
|
|
229
|
-
/** Exact durable egress fact and receipt selected by (tenant, device, event id). */
|
|
230
|
-
readAgentEgress(tenant: TenantId, deviceId: string, eventId: string): Promise<AgentEgressRecord | undefined>;
|
|
340
|
+
/** Exact durable egress fact and receipt selected by (tenant, device, AgentRef, event id). */
|
|
341
|
+
readAgentEgress(tenant: TenantId, deviceId: string, agentRef: AgentRef, eventId: string): Promise<AgentEgressRecord | undefined>;
|
|
231
342
|
/**
|
|
232
343
|
* Host control plane: the same first terminal, decoded into the typed read
|
|
233
344
|
* model ({@link TerminalResult}) so a host reads result fields, not envelope
|
|
@@ -20,6 +20,8 @@ export interface InMemoryByokCloudOptions {
|
|
|
20
20
|
readonly tokenSigner?: TokenSigner;
|
|
21
21
|
readonly capabilities?: CapabilityDeclaration;
|
|
22
22
|
readonly operatorId?: string;
|
|
23
|
+
/** Single-product instance authority; absent leaves the device row as the whole product authority. */
|
|
24
|
+
readonly instanceProductId?: string;
|
|
23
25
|
readonly maxBlobSizeBytes?: number;
|
|
24
26
|
readonly longPollHoldMs?: number;
|
|
25
27
|
readonly longPollIntervalMs?: number;
|
|
@@ -51,6 +53,8 @@ export interface InMemoryByokCloudOptions {
|
|
|
51
53
|
readonly skillPacks?: SkillPackStore;
|
|
52
54
|
readonly skillPackPageLimit?: number;
|
|
53
55
|
readonly agentMessage?: ByokCloudOptions['agentMessage'];
|
|
56
|
+
/** Post-commit relay; absent means nothing is watching. */
|
|
57
|
+
readonly observer?: ByokCloudOptions['observer'];
|
|
54
58
|
readonly agentMemoryProjectionAuthorizer?: AgentMemoryProjectionAuthorizer;
|
|
55
59
|
readonly agentMemoryProjectionStore?: AgentMemoryProjectionStore;
|
|
56
60
|
}
|
package/dist/coordination.d.ts
CHANGED
|
@@ -21,6 +21,14 @@ export declare function assertBoardLabels(channel: string, title: string, limits
|
|
|
21
21
|
readonly channelMaxBytes: number;
|
|
22
22
|
readonly titleMaxBytes: number;
|
|
23
23
|
}): void;
|
|
24
|
+
/**
|
|
25
|
+
* The page bound on {@link TaskAttemptStore.list}. Fail-closed rather than
|
|
26
|
+
* defaulted or clamped: a caller that asks for `0`, `-1`, `2.5`, or `Infinity`
|
|
27
|
+
* has a bug, and answering it with a silently substituted limit hides that bug
|
|
28
|
+
* behind a plausible page. Enforced in the STORE rather than only at the
|
|
29
|
+
* façade, because the port is reachable directly by a host composition.
|
|
30
|
+
*/
|
|
31
|
+
export declare function assertTaskAttemptListLimit(limit: number): void;
|
|
24
32
|
export declare function validateActivityEvents(events: readonly AgentEventOrUnknown[], dropped: number, bounds: ActivityBounds): void;
|
|
25
33
|
export declare function validateActivityBatch(input: Omit<ActivityAppendInput, 'ttlMs' | 'capacity'>, bounds: ActivityBounds): void;
|
|
26
34
|
export declare function appendActivityEvents(activity: TenantBoundActivity, input: {
|
package/dist/errors.d.ts
CHANGED
|
@@ -43,6 +43,15 @@ export declare const CLOUD_ERROR_CODES: {
|
|
|
43
43
|
readonly activity_batch_too_large: 'activity_batch_too_large';
|
|
44
44
|
/** Host control-plane task lookup is tenant-closed and found no task. */
|
|
45
45
|
readonly task_not_found: 'task_not_found';
|
|
46
|
+
/**
|
|
47
|
+
* A host `approveTask`/`rejectTask` names a task with no approval to
|
|
48
|
+
* resolve: the durable timeline holds no unresolved `approval_requested`
|
|
49
|
+
* (including a tail that has expired or was never written), the attempt has
|
|
50
|
+
* already reached a terminal status, or no device ever claimed it so there
|
|
51
|
+
* is nothing to notify. Fail-closed by construction — cloud never invents a
|
|
52
|
+
* pending approval to make the call succeed.
|
|
53
|
+
*/
|
|
54
|
+
readonly task_not_awaiting_approval: 'task_not_awaiting_approval';
|
|
46
55
|
/** Durable target-device capability is absent, revoked, or unavailable. */
|
|
47
56
|
readonly agent_capability_missing: 'agent_capability_missing';
|
|
48
57
|
/** Inbound Agent identity did not exactly match the offered identity. */
|
package/dist/handlers/blobs.d.ts
CHANGED
|
@@ -31,6 +31,8 @@ export declare const BLOB_RESERVATION_TTL_MS: number;
|
|
|
31
31
|
*/
|
|
32
32
|
export interface BlobContentRouteDeps {
|
|
33
33
|
readonly contentProxy: BlobContentProxy;
|
|
34
|
+
/** Absolute ceiling independent of an untrusted Content-Length header. */
|
|
35
|
+
readonly maxUploadBytes: number;
|
|
34
36
|
}
|
|
35
37
|
export declare function createBlobHandler(deps: BlobRouteDeps): (c: Context) => Promise<Response>;
|
|
36
38
|
export declare function finalizeBlobHandler(deps: BlobRouteDeps): (c: Context) => Promise<Response>;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `GET /byok/events?cursor=N` — the long-poll receive half (§8).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Four properties the daemon's transport depends on, reproduced against the
|
|
5
5
|
* core mailbox rather than an in-process outbox:
|
|
6
6
|
*
|
|
7
7
|
* - **Reading is not acknowledging.** `readAfter` never moves the cursor. The
|
|
@@ -11,14 +11,20 @@
|
|
|
11
11
|
* that hands back the same cursor replays the same page forever.
|
|
12
12
|
* - **The ack is monotonic.** A cursor at or below what the device already
|
|
13
13
|
* acked carries no new information and is not an ack attempt; only a
|
|
14
|
-
* strictly higher one advances. The store
|
|
15
|
-
*
|
|
16
|
-
*
|
|
14
|
+
* strictly higher one advances. The store stays the authority on both
|
|
15
|
+
* bounds: it rejects regression and refuses any cursor beyond the highest
|
|
16
|
+
* one this route recorded immediately before returning it.
|
|
17
17
|
* - **The hold.** An empty poll is held open instead of answered immediately,
|
|
18
18
|
* so an idle daemon is not a busy-loop. The hold is a bounded re-read, not a
|
|
19
19
|
* registered waiter: a waiter map is exactly the cross-request state a
|
|
20
20
|
* stateless handler may not keep (S3.5 boxes 14-15), and a second cloud
|
|
21
21
|
* instance would not see it anyway.
|
|
22
|
+
* - **A lost cursor fails closed.** A cursor below the mailbox's retained
|
|
23
|
+
* window is answered `409 { error: 'cursor_too_old', recoverableFrom }`, the
|
|
24
|
+
* same body the reference server returns (`packages/server/src/http.ts:386`)
|
|
25
|
+
* and the only one the daemon's transport can turn into a resync. Serving
|
|
26
|
+
* such a caller a 200 would hand it a partial tail it has no way to tell
|
|
27
|
+
* apart from a complete one.
|
|
22
28
|
*/
|
|
23
29
|
import type { Context } from 'hono';
|
|
24
30
|
import { type DeviceRouteDeps } from './shared';
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import type { Context } from 'hono';
|
|
23
23
|
import { type AgentContentReceiptPayload, type AgentMessageDispositionPayload, type AgentMessagePublishPayload } from '@byok-sdk/protocol';
|
|
24
|
+
import { type ByokCloudObserver } from '../inbound';
|
|
24
25
|
import type { ActivityBounds } from '../coordination';
|
|
25
26
|
import type { AgentEgressRecord } from '../stores/ports';
|
|
26
27
|
import type { TenantStores } from '../tenant-stores';
|
|
@@ -48,5 +49,11 @@ export interface MessagesRouteDeps extends DeviceRouteDeps {
|
|
|
48
49
|
}>;
|
|
49
50
|
};
|
|
50
51
|
readonly appendAgentMessageDisposition: (stores: TenantStores, deviceId: string, taskId: string, payload: AgentMessageDispositionPayload) => Promise<void>;
|
|
52
|
+
/**
|
|
53
|
+
* Post-commit relay. Batch order IS notification order: the loop below
|
|
54
|
+
* awaits one envelope's gate before starting the next, so an observer sees
|
|
55
|
+
* committed envelopes in the order the device sent them.
|
|
56
|
+
*/
|
|
57
|
+
readonly observer?: ByokCloudObserver;
|
|
51
58
|
}
|
|
52
59
|
export declare function messagesHandler(deps: MessagesRouteDeps): (c: Context) => Promise<Response>;
|