@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 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.
@@ -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
- export declare function validateTeamRoster(roster: unknown, path?: string): ProtocolViolation[];
19
- export declare function validateSnapshot(snapshot: unknown, manifest: unknown, path?: string): ProtocolViolation[];
20
- export declare function validateEventEnvelope(envelope: unknown, manifest: unknown, path?: string): ProtocolViolation[];
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[];
@@ -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)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",