@xema/omni-protocol 0.1.9 → 0.1.10
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 +11 -0
- package/dist/testing.js +5 -2
- package/dist/validation.d.ts +15 -4
- package/dist/validation.js +12 -9
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -40,6 +40,17 @@ assertNoViolations(violations);
|
|
|
40
40
|
A violation carries a stable `rule` id such as `task.browser.url.scheme`, the `path` it was found
|
|
41
41
|
at such as `snapshot.tasks[0].browsers[1].url`, and a `message`.
|
|
42
42
|
|
|
43
|
+
One rule needs more than the object in hand. A roster never carries the agent it is published to
|
|
44
|
+
— a lead does not report to themself — and a validator cannot know who that is from the roster
|
|
45
|
+
alone. `validateTeamRoster`, `validateSnapshot`, and `validateEventEnvelope` each take an optional
|
|
46
|
+
final `{ self }`, the signed-in agent's `AuthenticationState.identity.id`; given it, they report
|
|
47
|
+
`team.member.self` and `team.request.self`. Without it that rule is not checked. `exerciseAdapter`
|
|
48
|
+
always passes it.
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
validateSnapshot(snapshot, manifest, "snapshot", { self: authentication.identity.id });
|
|
52
|
+
```
|
|
53
|
+
|
|
43
54
|
## Conformance
|
|
44
55
|
|
|
45
56
|
`exerciseAdapter` validates the manifest, opens an authenticated session, connects, checks
|
package/dist/testing.js
CHANGED
|
@@ -34,6 +34,9 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
34
34
|
if (authenticationState.status !== "authenticated") {
|
|
35
35
|
throw new Error(`Adapter contract exercise requires authenticated test state, received ${authenticationState.status}`);
|
|
36
36
|
}
|
|
37
|
+
// The harness knows who is signed in, so it can hold the adapter to a rule the structural
|
|
38
|
+
// validators cannot check alone: a roster never carries the agent it is published to.
|
|
39
|
+
const reader = { self: authenticationState.identity.id };
|
|
37
40
|
connection = await adapter.connect(context);
|
|
38
41
|
const live = connection;
|
|
39
42
|
// The optional methods are optional only until something declares a need for them. Each
|
|
@@ -57,7 +60,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
57
60
|
requireMethod("openMedia", "the manifest channel is voice");
|
|
58
61
|
const eventIds = new Set();
|
|
59
62
|
unsubscribe = connection.subscribe(envelope => {
|
|
60
|
-
violations.push(...validateEventEnvelope(envelope, adapter.manifest));
|
|
63
|
+
violations.push(...validateEventEnvelope(envelope, adapter.manifest, "event", reader));
|
|
61
64
|
if (typeof envelope?.id === "string") {
|
|
62
65
|
if (eventIds.has(envelope.id))
|
|
63
66
|
return;
|
|
@@ -66,7 +69,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
|
|
|
66
69
|
events.push(envelope);
|
|
67
70
|
});
|
|
68
71
|
const snapshot = await connection.snapshot();
|
|
69
|
-
violations.push(...validateSnapshot(snapshot, adapter.manifest));
|
|
72
|
+
violations.push(...validateSnapshot(snapshot, adapter.manifest, "snapshot", reader));
|
|
70
73
|
if (snapshot?.sessionCapabilities?.breaks === true) {
|
|
71
74
|
// The four stand or fall together: `granted` is a promise to honour a later commit, and
|
|
72
75
|
// an adapter with requestBreak but no commitBreak leaves an agent a break that never starts.
|
package/dist/validation.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ProtocolViolation } from "./index.js";
|
|
1
|
+
import { type ProtocolViolation, type UserId } from "./index.js";
|
|
2
2
|
export type { ProtocolViolation } from "./index.js";
|
|
3
3
|
export declare class ProtocolConformanceError extends Error {
|
|
4
4
|
readonly violations: readonly ProtocolViolation[];
|
|
@@ -15,7 +15,18 @@ export interface TaskValidationContext {
|
|
|
15
15
|
channel: string;
|
|
16
16
|
}
|
|
17
17
|
export declare function validateTask(task: unknown, context: TaskValidationContext, path?: string): ProtocolViolation[];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Who is reading what the adapter published. The validators check structure without it; given
|
|
20
|
+
* `self`, they also check that a roster never carries the agent it is published to.
|
|
21
|
+
*/
|
|
22
|
+
export interface ReaderContext {
|
|
23
|
+
/**
|
|
24
|
+
* The signed-in agent, `AuthenticationState.identity.id`. A lead does not report to themself:
|
|
25
|
+
* a roster that lists them in `members`, or their own ask in `requests`, is a violation.
|
|
26
|
+
*/
|
|
27
|
+
self?: UserId;
|
|
28
|
+
}
|
|
29
|
+
export declare function validateTeamRoster(roster: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
|
|
30
|
+
export declare function validateSnapshot(snapshot: unknown, manifest: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
|
|
31
|
+
export declare function validateEventEnvelope(envelope: unknown, manifest: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
|
|
21
32
|
export declare function validateAuthenticationState(state: unknown, path?: string): ProtocolViolation[];
|
package/dist/validation.js
CHANGED
|
@@ -705,12 +705,12 @@ function validateBreakState(value, path, into) {
|
|
|
705
705
|
}
|
|
706
706
|
});
|
|
707
707
|
}
|
|
708
|
-
export function validateTeamRoster(roster, path = "team") {
|
|
708
|
+
export function validateTeamRoster(roster, path = "team", context = {}) {
|
|
709
709
|
const into = new Collector();
|
|
710
|
-
validateTeamRosterInto(roster, path, into);
|
|
710
|
+
validateTeamRosterInto(roster, path, context, into);
|
|
711
711
|
return into.violations;
|
|
712
712
|
}
|
|
713
|
-
function validateTeamRosterInto(roster, path, into) {
|
|
713
|
+
function validateTeamRosterInto(roster, path, context, into) {
|
|
714
714
|
if (!isPlainObject(roster)) {
|
|
715
715
|
into.add("team.shape", path, "a team roster must be an object");
|
|
716
716
|
return;
|
|
@@ -740,7 +740,9 @@ function validateTeamRosterInto(roster, path, into) {
|
|
|
740
740
|
into.add("team.request.unique", `${at}.id`, `duplicate request id: ${request.id}`);
|
|
741
741
|
seenRequests.add(request.id);
|
|
742
742
|
}
|
|
743
|
-
into.require(isUserId(request.memberId), "team.request.memberId", `${at}.memberId`, "a request names the member asking")
|
|
743
|
+
if (into.require(isUserId(request.memberId), "team.request.memberId", `${at}.memberId`, "a request names the member asking")) {
|
|
744
|
+
into.require(request.memberId !== context.self, "team.request.self", `${at}.memberId`, "the roster carries the reader's own ask: an agent's request for a lead goes to whoever leads them");
|
|
745
|
+
}
|
|
744
746
|
into.require(isTaskId(request.taskId), "team.request.taskId", `${at}.taskId`, "a request names the task the lead would join");
|
|
745
747
|
if (request.note !== undefined)
|
|
746
748
|
into.filled(request.note, "team.request.note", `${at}.note`, "a note must not be empty when present");
|
|
@@ -763,6 +765,7 @@ function validateTeamRosterInto(roster, path, into) {
|
|
|
763
765
|
if (seen.has(member.id))
|
|
764
766
|
into.add("team.member.unique", `${at}.id`, `duplicate roster member: ${member.id}`);
|
|
765
767
|
seen.add(member.id);
|
|
768
|
+
into.require(member.id !== context.self, "team.member.self", `${at}.id`, "the roster carries the agent it is published to: a lead does not report to themself");
|
|
766
769
|
}
|
|
767
770
|
into.oneOf(member.availability, TEAM_AVAILABILITIES, "team.member.availability", `${at}.availability`);
|
|
768
771
|
if (member.since !== undefined)
|
|
@@ -771,7 +774,7 @@ function validateTeamRosterInto(roster, path, into) {
|
|
|
771
774
|
into.oneOf(member.break, BREAK_APPROVALS, "team.member.break", `${at}.break`);
|
|
772
775
|
});
|
|
773
776
|
}
|
|
774
|
-
export function validateSnapshot(snapshot, manifest, path = "snapshot") {
|
|
777
|
+
export function validateSnapshot(snapshot, manifest, path = "snapshot", context = {}) {
|
|
775
778
|
const into = new Collector();
|
|
776
779
|
if (!isPlainObject(snapshot)) {
|
|
777
780
|
into.add("snapshot.shape", path, "a snapshot must be an object");
|
|
@@ -839,7 +842,7 @@ export function validateSnapshot(snapshot, manifest, path = "snapshot") {
|
|
|
839
842
|
}
|
|
840
843
|
}
|
|
841
844
|
if (snapshot.team !== undefined)
|
|
842
|
-
validateTeamRosterInto(snapshot.team, `${path}.team`, into);
|
|
845
|
+
validateTeamRosterInto(snapshot.team, `${path}.team`, context, into);
|
|
843
846
|
return into.violations;
|
|
844
847
|
}
|
|
845
848
|
// ---------------------------------------------------------------------------
|
|
@@ -912,7 +915,7 @@ function validateProviderSummary(value, path, into) {
|
|
|
912
915
|
into.require(typeof metric.value === "string", "event.summary.metric.value", `${at}.value`, "a metric value must be a string; the provider decides how it reads");
|
|
913
916
|
});
|
|
914
917
|
}
|
|
915
|
-
export function validateEventEnvelope(envelope, manifest, path = "event") {
|
|
918
|
+
export function validateEventEnvelope(envelope, manifest, path = "event", context = {}) {
|
|
916
919
|
const into = new Collector();
|
|
917
920
|
if (!isPlainObject(envelope)) {
|
|
918
921
|
into.add("event.shape", path, "an event envelope must be an object");
|
|
@@ -931,7 +934,7 @@ export function validateEventEnvelope(envelope, manifest, path = "event") {
|
|
|
931
934
|
switch (event.type) {
|
|
932
935
|
case "snapshot":
|
|
933
936
|
into.oneOf(event.reason, SNAPSHOT_REASONS, "event.snapshot.reason", `${at}.reason`);
|
|
934
|
-
into.violations.push(...validateSnapshot(event.snapshot, manifest, `${at}.snapshot
|
|
937
|
+
into.violations.push(...validateSnapshot(event.snapshot, manifest, `${at}.snapshot`, context));
|
|
935
938
|
break;
|
|
936
939
|
case "provider-status":
|
|
937
940
|
into.oneOf(event.status, CONNECTION_STATUSES, "event.providerStatus.status", `${at}.status`);
|
|
@@ -975,7 +978,7 @@ export function validateEventEnvelope(envelope, manifest, path = "event") {
|
|
|
975
978
|
validateProviderSummary(event.summary, `${at}.summary`, into);
|
|
976
979
|
break;
|
|
977
980
|
case "team-updated":
|
|
978
|
-
validateTeamRosterInto(event.team, `${at}.team`, into);
|
|
981
|
+
validateTeamRosterInto(event.team, `${at}.team`, context, into);
|
|
979
982
|
break;
|
|
980
983
|
case "contacts-updated":
|
|
981
984
|
if (!Array.isArray(event.contacts)) {
|