@xema/omni-protocol 0.1.14 → 0.1.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/testing.js +5 -2
- package/dist/validation.d.ts +9 -0
- package/dist/validation.js +71 -4
- package/guide.md +8 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -30,7 +30,8 @@ An adapter is loaded from a separate package and may be compiled against a diffe
|
|
|
30
30
|
version, so its output is untrusted input. Every validator takes `unknown` and returns every
|
|
31
31
|
violation it found rather than throwing on the first, so a caller reports all of them at once.
|
|
32
32
|
Validating a snapshot before it replaces provider state is what stops a malformed task reaching
|
|
33
|
-
the agent's workspace
|
|
33
|
+
the agent's workspace; validating a result with `validateResult(result, method)` before acting on
|
|
34
|
+
it is what stops a status the host does not know being shown as an outcome.
|
|
34
35
|
|
|
35
36
|
```ts
|
|
36
37
|
const violations = validateSnapshot(snapshot, manifest);
|
package/dist/testing.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { browserSessionKey, sameCapabilities, } from "./index.js";
|
|
2
|
-
import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateManifest, validateSnapshot, } from "./validation.js";
|
|
2
|
+
import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateManifest, validateResult, validateSnapshot, } from "./validation.js";
|
|
3
3
|
export { ProtocolConformanceError, assertNoViolations } from "./validation.js";
|
|
4
4
|
/**
|
|
5
5
|
* A part of the contract a run may never reach: state nothing obliges an adapter to publish, so
|
|
@@ -245,7 +245,10 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
245
245
|
// Capacity is stated, not requested: nothing may be allocated until it is, so a connection
|
|
246
246
|
// that will not accept one is a connection nothing can be given to.
|
|
247
247
|
const capacity = await connection.setCapacity({ count: 1 });
|
|
248
|
-
|
|
248
|
+
const malformed = validateResult(capacity, "setCapacity", "connection.setCapacity");
|
|
249
|
+
violations.push(...malformed);
|
|
250
|
+
// A refusal is read only from a result that has the shape of one.
|
|
251
|
+
if (malformed.length === 0 && capacity.status === "failed") {
|
|
249
252
|
violations.push({
|
|
250
253
|
rule: "connection.setCapacity.failed",
|
|
251
254
|
path: "connection.setCapacity",
|
package/dist/validation.d.ts
CHANGED
|
@@ -34,4 +34,13 @@ export interface ReaderContext {
|
|
|
34
34
|
export declare function validateTeamRoster(roster: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
|
|
35
35
|
export declare function validateSnapshot(snapshot: unknown, manifest: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
|
|
36
36
|
export declare function validateEventEnvelope(envelope: unknown, manifest: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
|
|
37
|
+
/** The connection methods whose results `validateResult` knows. */
|
|
38
|
+
export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeamBreak" | "executeTeamConsult" | "openMedia";
|
|
39
|
+
/**
|
|
40
|
+
* Validates what a connection method answered. A result is untrusted for the same reason a
|
|
41
|
+
* snapshot is: it comes from an adapter that may be compiled against another version, and Omni
|
|
42
|
+
* shows the agent what it says. A status the method does not answer, a failure status without a
|
|
43
|
+
* failure, a success carrying one, or an `omni.` code the contract lacks are each refused.
|
|
44
|
+
*/
|
|
45
|
+
export declare function validateResult(result: unknown, method: ResultMethod, path?: string): ProtocolViolation[];
|
|
37
46
|
export declare function validateAuthenticationState(state: unknown, path?: string): ProtocolViolation[];
|
package/dist/validation.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
// Each list is pinned to its type both ways -- a member the type lacks, or a member the list
|
|
10
10
|
// lacks, fails to compile -- so what the validators accept cannot drift from what the
|
|
11
11
|
// declarations say.
|
|
12
|
-
import { ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, IDLE_CAPABILITIES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, negotiateProtocolVersion, } from "./index.js";
|
|
12
|
+
import { ALLOWED_BROWSER_URL_SCHEMES, BREAK_KINDS, BROWSER_ISOLATION_SCHEMES, IDLE_CAPABILITIES, OMNI_FAILURE_CODES, OMNI_SUPPORTED_PROTOCOL_VERSIONS, negotiateProtocolVersion, } from "./index.js";
|
|
13
13
|
export class ProtocolConformanceError extends Error {
|
|
14
14
|
violations;
|
|
15
15
|
constructor(violations, summary = "Adapter violates the Omni protocol") {
|
|
@@ -895,9 +895,7 @@ function validateTaskOutcome(value, path, into) {
|
|
|
895
895
|
into.add("event.taskEnded.outcome.failed", `${path}.failure`, "a failed outcome must carry a failure");
|
|
896
896
|
}
|
|
897
897
|
else {
|
|
898
|
-
|
|
899
|
-
into.filled(value.failure.message, "failure.message", `${path}.failure.message`, "a failure needs a message");
|
|
900
|
-
into.require(typeof value.failure.retryable === "boolean", "failure.retryable", `${path}.failure.retryable`, "a failure must say whether it is retryable");
|
|
898
|
+
validateFailureInto(value.failure, `${path}.failure`, into);
|
|
901
899
|
}
|
|
902
900
|
break;
|
|
903
901
|
default:
|
|
@@ -1019,6 +1017,75 @@ export function validateEventEnvelope(envelope, manifest, path = "event", contex
|
|
|
1019
1017
|
return into.violations;
|
|
1020
1018
|
}
|
|
1021
1019
|
// ---------------------------------------------------------------------------
|
|
1020
|
+
// Results. A result crosses the same boundary a snapshot does, from an adapter that may be
|
|
1021
|
+
// compiled against another version, and Omni shows the agent what it says.
|
|
1022
|
+
// ---------------------------------------------------------------------------
|
|
1023
|
+
/** A `ProtocolFailure`, wherever one appears: on a result, or on a task's failed outcome. */
|
|
1024
|
+
function validateFailureInto(value, path, into) {
|
|
1025
|
+
if (!isPlainObject(value)) {
|
|
1026
|
+
into.add("failure.shape", path, "a failure must be an object");
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
if (into.filled(value.code, "failure.code", `${path}.code`, "a failure needs a code")) {
|
|
1030
|
+
// A provider names its own codes freely; the `omni.` namespace is the contract's, and a code
|
|
1031
|
+
// in it that the contract lacks is one Omni would show without knowing what it means.
|
|
1032
|
+
if (value.code.startsWith("omni.")) {
|
|
1033
|
+
into.require(OMNI_FAILURE_CODES.includes(value.code), "failure.code.unknown", `${path}.code`, `not a contract failure code: ${value.code}`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
into.filled(value.message, "failure.message", `${path}.message`, "a failure needs a message");
|
|
1037
|
+
into.require(typeof value.retryable === "boolean", "failure.retryable", `${path}.retryable`, "a failure must say whether it is retryable");
|
|
1038
|
+
if (value.retryAfterMs !== undefined) {
|
|
1039
|
+
into.require(typeof value.retryAfterMs === "number" && Number.isFinite(value.retryAfterMs) && value.retryAfterMs >= 0, "failure.retryAfterMs", `${path}.retryAfterMs`, "retryAfterMs must be a non-negative number when present");
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
// Pinned to the result unions: each method's one success status, and the status that carries a
|
|
1043
|
+
// failure. A method added to `Connection` without a row here is a compile error at the call site.
|
|
1044
|
+
const RESULT_STATUSES = {
|
|
1045
|
+
execute: { success: "applied", failure: "failed" },
|
|
1046
|
+
dial: { success: "dialled", failure: "failed" },
|
|
1047
|
+
setCapacity: { success: "accepted", failure: "failed" },
|
|
1048
|
+
requestBreak: { success: "requested", failure: "failed" },
|
|
1049
|
+
commitBreak: { success: "committed", failure: "failed" },
|
|
1050
|
+
cancelBreak: { success: "cancelled", failure: "failed" },
|
|
1051
|
+
endBreak: { success: "ended", failure: "failed" },
|
|
1052
|
+
executeTeamBreak: { success: "applied", failure: "failed" },
|
|
1053
|
+
executeTeamConsult: { success: "applied", failure: "failed" },
|
|
1054
|
+
openMedia: { success: "opened", failure: "unavailable" },
|
|
1055
|
+
};
|
|
1056
|
+
/**
|
|
1057
|
+
* Validates what a connection method answered. A result is untrusted for the same reason a
|
|
1058
|
+
* snapshot is: it comes from an adapter that may be compiled against another version, and Omni
|
|
1059
|
+
* shows the agent what it says. A status the method does not answer, a failure status without a
|
|
1060
|
+
* failure, a success carrying one, or an `omni.` code the contract lacks are each refused.
|
|
1061
|
+
*/
|
|
1062
|
+
export function validateResult(result, method, path = "result") {
|
|
1063
|
+
const into = new Collector();
|
|
1064
|
+
const statuses = RESULT_STATUSES[method];
|
|
1065
|
+
if (!isPlainObject(result)) {
|
|
1066
|
+
into.add("result.shape", path, `${method} must answer an object`);
|
|
1067
|
+
return into.violations;
|
|
1068
|
+
}
|
|
1069
|
+
if (result.status === statuses.success) {
|
|
1070
|
+
into.require(result.failure === undefined, "result.failure.unexpected", `${path}.failure`, `${statuses.success} carries no failure`);
|
|
1071
|
+
if (method === "openMedia") {
|
|
1072
|
+
into.require(isPlainObject(result.session), "result.session", `${path}.session`, "opened carries the media session");
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
else if (result.status === statuses.failure) {
|
|
1076
|
+
if (result.failure === undefined) {
|
|
1077
|
+
into.add("result.failure.required", `${path}.failure`, `${statuses.failure} carries the failure that says why`);
|
|
1078
|
+
}
|
|
1079
|
+
else {
|
|
1080
|
+
validateFailureInto(result.failure, `${path}.failure`, into);
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
else {
|
|
1084
|
+
into.add("result.status", `${path}.status`, `${method} answers ${statuses.success} or ${statuses.failure}, not ${String(result.status)}`);
|
|
1085
|
+
}
|
|
1086
|
+
return into.violations;
|
|
1087
|
+
}
|
|
1088
|
+
// ---------------------------------------------------------------------------
|
|
1022
1089
|
// Authentication.
|
|
1023
1090
|
// ---------------------------------------------------------------------------
|
|
1024
1091
|
function validateUser(value, rule, path, into) {
|
package/guide.md
CHANGED
|
@@ -2225,7 +2225,8 @@ Starts one outbound call from the idle dialpad. It is present only when the voic
|
|
|
2225
2225
|
declares `dial`.
|
|
2226
2226
|
|
|
2227
2227
|
- `destination` is the original number selected or entered by the agent.
|
|
2228
|
-
-
|
|
2228
|
+
- The provider holds `destination` to its declared `destinationPolicy`: under `contacts-only`, a
|
|
2229
|
+
number that is not one of its contacts answers `failed` with `omni.destination-not-permitted`.
|
|
2229
2230
|
- `dialled` confirms that outbound call creation completed.
|
|
2230
2231
|
- `failed` contains a `ProtocolFailure` and confirms no call was placed.
|
|
2231
2232
|
|
|
@@ -2934,6 +2935,11 @@ Applies a `TaskCommandRequest` to one provider-local task.
|
|
|
2934
2935
|
deciding a member's break that another lead has already decided is `applied` when the decisions
|
|
2935
2936
|
agree and `failed`, saying so in `message`, when they differ. `commitBreak()` on a break already
|
|
2936
2937
|
in effect is `committed` for the same reason.
|
|
2938
|
+
- **A result is untrusted for the same reason a snapshot is.** It comes from an adapter that may be
|
|
2939
|
+
compiled against another version, and Omni shows the agent what it says. Omni validates it at
|
|
2940
|
+
the boundary with `validateResult(result, "execute")` — a status the method does not answer, a
|
|
2941
|
+
`failed` without its failure, a success carrying one, or an `omni.` code this contract lacks is
|
|
2942
|
+
refused, and the command is treated as unsettled.
|
|
2937
2943
|
- **A settled result is a fact; an unsettled promise is not.** Transport uncertainty may reject the
|
|
2938
2944
|
promise with no result at all, and that means *unknown*, not *failed*, and a snapshot follows —
|
|
2939
2945
|
see **An unsettled result is unknown**. `failed` must never be returned for something the
|
|
@@ -3176,6 +3182,7 @@ same exported checks are used by Omni and adapter tests so their interpretations
|
|
|
3176
3182
|
| `validateEventEnvelope(envelope, manifest)` | Envelope identity, timestamp, and the payload for each event type. |
|
|
3177
3183
|
| `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
|
|
3178
3184
|
| `validateScheduledActivity(activity)` | Required activity fields and start/end ordering. |
|
|
3185
|
+
| `validateResult(result, method)` | What a connection method answered: the status it gives, a failure where the status says so and nowhere else, the failure's shape, and that an `omni.` code is one this contract names. |
|
|
3179
3186
|
| `validateAuthenticationState(state)` | The identity each state must carry, the capabilities a usable login declares, and the expiry that only `authenticated` may. |
|
|
3180
3187
|
|
|
3181
3188
|
Each returns `ProtocolViolation[]` rather than throwing, so a caller can report every problem at
|