@xema/omni-protocol 0.1.27 → 0.1.28

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/index.d.ts CHANGED
@@ -62,7 +62,7 @@ export interface BrowserAccess {
62
62
  }
63
63
  export interface PersonalBrowserCapability {
64
64
  access: BrowserAccess;
65
- accessPolicyScope?: "initial-url" | "all-navigation";
65
+ accessAppliesTo?: "initial-url" | "all-navigation";
66
66
  }
67
67
  export type DialDestinations = "contacts-only" | "any-number";
68
68
  export interface DialCapability {
@@ -237,7 +237,7 @@ export type CompleteAuthenticationResult = {
237
237
  failure: AuthenticationFailure;
238
238
  };
239
239
  export type AuthenticationActionResult = {
240
- status: "accepted";
240
+ status: "applied";
241
241
  } | {
242
242
  status: "failed";
243
243
  failure: AuthenticationFailure;
@@ -262,7 +262,7 @@ export interface AuthenticationSession {
262
262
  export type HostAudioInput =
263
263
  /** Omni has the microphone. `flowing` is false while the hardware or OS says no audio moves through it. */
264
264
  {
265
- status: "ready";
265
+ status: "available";
266
266
  localAudio: MediaStream;
267
267
  flowing: boolean;
268
268
  }
@@ -281,7 +281,7 @@ export type HostAudioUnavailableReason = "no-device" | "denied" | "not-asked" |
281
281
  /** Why the host has no speaker: no device, or one that was removed. */
282
282
  export type HostOutputUnavailableReason = "no-device" | "lost";
283
283
  export type HostAudioOutput = {
284
- status: "ready";
284
+ status: "available";
285
285
  } | {
286
286
  status: "unavailable";
287
287
  reason: HostOutputUnavailableReason;
@@ -303,7 +303,20 @@ export interface HostReport {
303
303
  };
304
304
  }
305
305
  /** The host, as an adapter may ask it: a report now, and every change for the life of the connection. */
306
+ /**
307
+ * What the host promises the provider, declared once per connection. Presence is the guarantee:
308
+ * an absent key is a host that makes no such promise, and a provider that needs one checks before
309
+ * it acts -- tokenising a URL it would otherwise send in the clear, or declining to offer work
310
+ * that only a person may accept.
311
+ */
312
+ export interface HostGuarantees {
313
+ /** Every task browser's `urlVisibility` is honoured in this host's chrome, tab by tab. */
314
+ browserUrlVisibility?: true;
315
+ /** A `consent` offer is accepted only by the person's own explicit act, never on their behalf. */
316
+ personConsent?: true;
317
+ }
306
318
  export interface Host {
319
+ guarantees: HostGuarantees;
307
320
  report(): HostReport;
308
321
  subscribe(listener: (report: HostReport) => void): Unsubscribe;
309
322
  }
@@ -353,7 +366,7 @@ export interface DispositionCode {
353
366
  }
354
367
  export interface DispositionRules {
355
368
  required?: boolean;
356
- notes?: "required" | "optional" | "hidden";
369
+ notes?: "required" | "optional" | "none";
357
370
  codes?: DispositionCode[];
358
371
  }
359
372
  export interface Destination {
@@ -609,8 +622,13 @@ export type Task<C extends Channel = Channel> = {
609
622
  assisting?: never;
610
623
  media?: never;
611
624
  });
612
- /** What the provider wants of Omni's acceptance policy for one offer. */
613
- export type AcceptanceMode = "no-preference" | "require-agent-acceptance" | "require-automatic-acceptance";
625
+ /**
626
+ * What the provider wants of Omni's acceptance policy for one offer. Present only where Omni was
627
+ * willing to accept for the agent (`autoAcceptTasks: true`): `consent` is therefore always the
628
+ * provider's requirement of an explicit acceptance, never Omni's own policy, which travels as an
629
+ * absent field.
630
+ */
631
+ export type AcceptanceMode = "no-preference" | "consent" | "automatic";
614
632
  export type TaskOutcome = {
615
633
  type: "completed";
616
634
  by: "agent" | "provider";
@@ -827,7 +845,7 @@ export interface BreakState {
827
845
  imposed?: ImposedBreak;
828
846
  }
829
847
  export type CapacityResult = {
830
- status: "accepted";
848
+ status: "applied";
831
849
  } | {
832
850
  status: "failed";
833
851
  failure: ProtocolFailure;
@@ -888,7 +906,7 @@ export interface TeamRoster {
888
906
  /** A capability a team policy can name: any task control, new call, or a skill by its provider id. */
889
907
  export type PolicyKey = Exclude<keyof TaskCapabilities<"voice">, keyof SharedTaskCapabilities> | Exclude<keyof TaskCapabilities<"chat">, keyof SharedTaskCapabilities> | Exclude<keyof TaskCapabilities<"email">, keyof SharedTaskCapabilities> | "dial" | `skill:${string}`;
890
908
  /** On for everyone, off for everyone, or the agent's own choice. Only `hold`, `mute` and skills may be `agent`. */
891
- export type TeamPolicySetting = "on" | "off" | "agent";
909
+ export type TeamPolicySetting = "on" | "off" | "person";
892
910
  /** One policy as the lead sees it: the setting, who set it, and `lockedBy` when a level above the team made it theirs to keep. */
893
911
  export interface TeamPolicy extends Resolved {
894
912
  setting: TeamPolicySetting;
package/dist/testing.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Host, type HostReport, type Manifest, type ProviderEvent } from "./index.js";
1
+ import { type Adapter, type AuthenticationState, type ProviderEventEnvelope, type Snapshot, type TaskCompletion, type BreakApproval, type BrowserSessionKeyInput, type Channel, type ConnectContext, type Host, type HostGuarantees, type HostReport, type Manifest, type ProviderEvent } from "./index.js";
2
2
  import { type ProtocolViolation } from "./validation.js";
3
3
  export { ProtocolConformanceError, assertNoViolations, type ProtocolViolation } from "./validation.js";
4
4
  /**
@@ -126,7 +126,7 @@ export declare function assertBreakFollowsItsRequests(envelopes: readonly Provid
126
126
  */
127
127
  export declare function assertMediaFollowsTheTask(envelopes: readonly ProviderEventEnvelope[], snapshot?: Snapshot): void;
128
128
  /** A host that reports one thing and never changes: what most adapter tests hand `exerciseAdapter`. */
129
- export declare function stillHost(report?: HostReport): Host;
129
+ export declare function stillHost(report?: HostReport, guarantees?: HostGuarantees): Host;
130
130
  /** One provider as the host sees it when freezing a break attempt's participant set. */
131
131
  export interface BreakCandidate {
132
132
  id: string;
package/dist/testing.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { browserSessionKey, effectiveLevels, sameCapabilities, } from "./index.js";
2
- import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateHostReport, validateManifest, validateResult, validateSnapshot, } from "./validation.js";
2
+ import { assertNoViolations, validateAuthenticationState, validateEventEnvelope, validateHostGuarantees, validateHostReport, 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
@@ -246,6 +246,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
246
246
  // testing a host that cannot exist. Its first report and every later one are validated, a
247
247
  // voice connection's host reports its audio and no other does, and the host the adapter
248
248
  // receives is wrapped so the harness can tell whether the adapter ever asked.
249
+ violations.push(...validateHostGuarantees(context.host.guarantees, "context.host.guarantees"));
249
250
  const first = context.host.report();
250
251
  violations.push(...validateHostReport(first, "context.host"));
251
252
  const hasAudio = isRecord(first) && first.audio !== undefined;
@@ -260,6 +261,7 @@ export async function exerciseAdapter(adapter, context, options = {}) {
260
261
  });
261
262
  let consulted = false;
262
263
  const host = {
264
+ guarantees: context.host.guarantees,
263
265
  report: () => { consulted = true; return context.host.report(); },
264
266
  subscribe: listener => { consulted = true; return context.host.subscribe(listener); },
265
267
  };
@@ -769,8 +771,8 @@ export function assertMediaFollowsTheTask(envelopes, snapshot) {
769
771
  assertNoViolations(found, "The media follows the task");
770
772
  }
771
773
  /** A host that reports one thing and never changes: what most adapter tests hand `exerciseAdapter`. */
772
- export function stillHost(report = { online: true }) {
773
- return { report: () => report, subscribe: () => () => undefined };
774
+ export function stillHost(report = { online: true }, guarantees = {}) {
775
+ return { guarantees, report: () => report, subscribe: () => () => undefined };
774
776
  }
775
777
  const usableLogin = (status) => status === "authenticated" || status === "refreshing";
776
778
  /**
@@ -43,10 +43,10 @@ export declare function validateTeamRoster(roster: unknown, path?: string, conte
43
43
  export declare function validateSnapshot(snapshot: unknown, manifest: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
44
44
  export declare function validateEventEnvelope(envelope: unknown, manifest: unknown, path?: string, context?: ReaderContext): ProtocolViolation[];
45
45
  /**
46
- * Validates the host's report as Omni publishes it to an adapter. This is Omni's output, so the
47
- * check belongs to the host's own tests and to the harness, which validates whatever host a test
48
- * hands the adapter.
46
+ * What a host promises. Presence is the guarantee, so a key declared `false` is refused: a
47
+ * promise withheld is an absent key, never a false one, exactly as a capability is.
49
48
  */
49
+ export declare function validateHostGuarantees(guarantees: unknown, path?: string): ProtocolViolation[];
50
50
  export declare function validateHostReport(report: unknown, path?: string): ProtocolViolation[];
51
51
  /** The connection methods whose results `validateResult` knows. */
52
52
  export type ResultMethod = "execute" | "dial" | "setCapacity" | "requestBreak" | "commitBreak" | "cancelBreak" | "endBreak" | "executeTeamBreak" | "executeTeamConsult" | "openMedia" | "setPreference" | "executeTeamPolicy";
@@ -42,7 +42,7 @@ const TASK_PHASES = membersOf({
42
42
  });
43
43
  const COMPLETION_MODES = membersOf({ "agent-command": true, "provider-automatic": true });
44
44
  const ACCEPTANCE_MODES = membersOf({
45
- "no-preference": true, "require-agent-acceptance": true, "require-automatic-acceptance": true,
45
+ "no-preference": true, "consent": true, "automatic": true,
46
46
  });
47
47
  const TRANSPORT_STATUSES = membersOf({ connecting: true, active: true, error: true });
48
48
  const AUTHENTICATION_METHODS = membersOf({ "browser-sso": true, credentials: true });
@@ -61,9 +61,9 @@ const HANDLING_STEPS = membersOf({
61
61
  const DESTINATION_KINDS = membersOf({ queue: true, agent: true, external: true });
62
62
  const CUSTOM_UI_CONTROLS = membersOf({ button: true, toggle: true, "menu-item": true });
63
63
  const CUSTOM_UI_PLACEMENTS = membersOf({ primary: true, secondary: true, overflow: true });
64
- const NOTES_POLICIES = membersOf({ required: true, optional: true, hidden: true });
64
+ const NOTES_RULES = membersOf({ required: true, optional: true, none: true });
65
65
  const ACCESS_MODES = membersOf({ "allow-all": true, "block-all": true });
66
- const ACCESS_POLICY_SCOPES = membersOf({
66
+ const ACCESS_APPLIES_TO = membersOf({
67
67
  "initial-url": true, "all-navigation": true,
68
68
  });
69
69
  const DIAL_DESTINATION_POLICIES = membersOf({ "contacts-only": true, "any-number": true });
@@ -247,8 +247,8 @@ function validateIdleCapabilities(value, channel, path, into) {
247
247
  }
248
248
  else {
249
249
  validateBrowserAccess(browser.access, `${path}.personalBrowser.access`, into);
250
- if (browser.accessPolicyScope !== undefined) {
251
- into.oneOf(browser.accessPolicyScope, ACCESS_POLICY_SCOPES, "manifest.personalBrowser.accessPolicyScope", `${path}.personalBrowser.accessPolicyScope`);
250
+ if (browser.accessAppliesTo !== undefined) {
251
+ into.oneOf(browser.accessAppliesTo, ACCESS_APPLIES_TO, "manifest.personalBrowser.accessAppliesTo", `${path}.personalBrowser.accessAppliesTo`);
252
252
  }
253
253
  }
254
254
  }
@@ -417,7 +417,7 @@ function validateDispositions(value, path, into) {
417
417
  into.add("task.dispositions.required.codes", `${path}.codes`, "a required disposition policy must publish at least one code");
418
418
  }
419
419
  if (value.notes !== undefined)
420
- into.oneOf(value.notes, NOTES_POLICIES, "task.dispositions.notes", `${path}.notes`);
420
+ into.oneOf(value.notes, NOTES_RULES, "task.dispositions.notes", `${path}.notes`);
421
421
  if (value.codes === undefined)
422
422
  return;
423
423
  if (!Array.isArray(value.codes)) {
@@ -486,11 +486,11 @@ function validateCustomCapabilities(value, path, into) {
486
486
  });
487
487
  }
488
488
  const DEFAULT_LEVEL_IDS = DEFAULT_LEVELS.map(level => level.id);
489
- const POLICY_SETTINGS = membersOf({ on: true, off: true, agent: true });
489
+ const POLICY_SETTINGS = membersOf({ on: true, off: true, person: true });
490
490
  const POLICY_KEYS = new Set([
491
491
  ...TASK_CAPABILITIES.voice, ...TASK_CAPABILITIES.chat, ...TASK_CAPABILITIES.email, "dial",
492
492
  ].filter(name => name !== "browsers" && name !== "dispositions" && name !== "custom"));
493
- const AGENT_SETTABLE = /^(hold|mute|skill:.+)$/;
493
+ const PERSON_SETTABLE = /^(hold|mute|skill:.+)$/;
494
494
  const isLocked = (value) => isPlainObject(value) && value.lockedBy !== undefined;
495
495
  /** The level ids in force: the manifest's, or the defaults when the caller holds no manifest. */
496
496
  const levelIds = (levels) => levels ?? DEFAULT_LEVEL_IDS;
@@ -1320,7 +1320,7 @@ function validateTeamPoliciesInto(value, path, into, levels) {
1320
1320
  continue;
1321
1321
  }
1322
1322
  if (into.oneOf(policy.setting, POLICY_SETTINGS, "team.policy.setting", `${at}.setting`)) {
1323
- into.require(policy.setting !== "agent" || AGENT_SETTABLE.test(key), "team.policy.agent", `${at}.setting`, `${key} is the team's, on or off; only hold, mute and skills may be left to the person`);
1323
+ into.require(policy.setting !== "person" || PERSON_SETTABLE.test(key), "team.policy.person", `${at}.setting`, `${key} is the team's, on or off; only hold, mute and skills may be left to the person`);
1324
1324
  }
1325
1325
  validateResolvedInto(policy, "team.policy", at, levels, into);
1326
1326
  into.require(policy.setBy !== "person", "team.policy.setBy", `${at}.setBy`, "a team policy is not set by a person");
@@ -1361,6 +1361,26 @@ function validateUnavailable(value, rule, path, into) {
1361
1361
  * check belongs to the host's own tests and to the harness, which validates whatever host a test
1362
1362
  * hands the adapter.
1363
1363
  */
1364
+ const HOST_GUARANTEES = membersOf({ browserUrlVisibility: true, personConsent: true });
1365
+ /**
1366
+ * What a host promises. Presence is the guarantee, so a key declared `false` is refused: a
1367
+ * promise withheld is an absent key, never a false one, exactly as a capability is.
1368
+ */
1369
+ export function validateHostGuarantees(guarantees, path = "host.guarantees") {
1370
+ const into = new Collector();
1371
+ if (!isPlainObject(guarantees)) {
1372
+ into.add("host.guarantees.shape", path, "a host declares its guarantees as an object, empty when it makes none");
1373
+ return into.violations;
1374
+ }
1375
+ for (const [name, declared] of Object.entries(guarantees)) {
1376
+ if (declared === undefined)
1377
+ continue;
1378
+ if (!into.require(HOST_GUARANTEES.includes(name), "host.guarantee.unknown", `${path}.${name}`, `${name} is not a guarantee this contract names: ${HOST_GUARANTEES.join(", ")}`))
1379
+ continue;
1380
+ into.require(declared === true, "host.guarantee.value", `${path}.${name}`, "a guarantee is declared by presence; one the host does not make is omitted, never false");
1381
+ }
1382
+ return into.violations;
1383
+ }
1364
1384
  export function validateHostReport(report, path = "host") {
1365
1385
  const into = new Collector();
1366
1386
  if (!isPlainObject(report)) {
@@ -1379,7 +1399,7 @@ export function validateHostReport(report, path = "host") {
1379
1399
  if (!isPlainObject(input)) {
1380
1400
  into.add("host.audio.input.shape", at, "audio carries its input");
1381
1401
  }
1382
- else if (input.status === "ready") {
1402
+ else if (input.status === "available") {
1383
1403
  into.require(typeof input.localAudio === "object" && input.localAudio !== null, "host.audio.input.localAudio", `${at}.localAudio`, "a ready input carries the captured microphone");
1384
1404
  into.require(typeof input.flowing === "boolean", "host.audio.input.flowing", `${at}.flowing`, "a ready input says whether audio is flowing through it");
1385
1405
  into.require(input.failure === undefined, "host.audio.input.failure.unexpected", `${at}.failure`, "a ready input carries no failure");
@@ -1392,14 +1412,14 @@ export function validateHostReport(report, path = "host") {
1392
1412
  into.require(input.flowing === undefined, "host.audio.input.flowing.unexpected", `${at}.flowing`, "an unavailable input has nothing to flow");
1393
1413
  }
1394
1414
  else {
1395
- into.add("host.audio.input.status", `${at}.status`, `an input is ready or unavailable, not ${String(input.status)}`);
1415
+ into.add("host.audio.input.status", `${at}.status`, `an input is available or unavailable, not ${String(input.status)}`);
1396
1416
  }
1397
1417
  const output = report.audio.output;
1398
1418
  const out = `${path}.audio.output`;
1399
1419
  if (!isPlainObject(output)) {
1400
1420
  into.add("host.audio.output.shape", out, "audio carries its output");
1401
1421
  }
1402
- else if (output.status === "ready") {
1422
+ else if (output.status === "available") {
1403
1423
  into.require(output.failure === undefined, "host.audio.output.failure.unexpected", `${out}.failure`, "a ready output carries no failure");
1404
1424
  into.require(output.reason === undefined, "host.audio.output.reason.unexpected", `${out}.reason`, "a ready output has no reason to be unavailable");
1405
1425
  }
@@ -1408,7 +1428,7 @@ export function validateHostReport(report, path = "host") {
1408
1428
  validateUnavailable(output, "host.audio.output", out, into);
1409
1429
  }
1410
1430
  else {
1411
- into.add("host.audio.output.status", `${out}.status`, `an output is ready or unavailable, not ${String(output.status)}`);
1431
+ into.add("host.audio.output.status", `${out}.status`, `an output is available or unavailable, not ${String(output.status)}`);
1412
1432
  }
1413
1433
  return into.violations;
1414
1434
  }
@@ -1417,7 +1437,7 @@ export function validateHostReport(report, path = "host") {
1417
1437
  const RESULT_STATUSES = {
1418
1438
  execute: { success: "applied", failure: "failed" },
1419
1439
  dial: { success: "dialled", failure: "failed" },
1420
- setCapacity: { success: "accepted", failure: "failed" },
1440
+ setCapacity: { success: "applied", failure: "failed" },
1421
1441
  requestBreak: { success: "requested", failure: "failed" },
1422
1442
  commitBreak: { success: "committed", failure: "failed" },
1423
1443
  cancelBreak: { success: "cancelled", failure: "failed" },
package/guide.md CHANGED
@@ -155,7 +155,7 @@ type BrowserAccess = {
155
155
 
156
156
  type PersonalBrowserCapability = {
157
157
  access: BrowserAccess;
158
- accessPolicyScope?: "initial-url" | "all-navigation";
158
+ accessAppliesTo?: "initial-url" | "all-navigation";
159
159
  };
160
160
 
161
161
  type DialDestinations = "contacts-only" | "any-number";
@@ -253,11 +253,11 @@ type HostAudioUnavailableReason = "no-device" | "denied" | "not-asked" | "in-use
253
253
  type HostOutputUnavailableReason = "no-device" | "lost";
254
254
 
255
255
  type HostAudioInput =
256
- | { status: "ready"; localAudio: MediaStream; flowing: boolean }
256
+ | { status: "available"; localAudio: MediaStream; flowing: boolean }
257
257
  | { status: "unavailable"; reason: HostAudioUnavailableReason; failure: ProtocolFailure };
258
258
 
259
259
  type HostAudioOutput =
260
- | { status: "ready" }
260
+ | { status: "available" }
261
261
  | { status: "unavailable"; reason: HostOutputUnavailableReason; failure: ProtocolFailure };
262
262
 
263
263
  type UrlVisibility = "full" | "domain" | "hidden";
@@ -270,7 +270,13 @@ type HostReport = {
270
270
  };
271
271
  };
272
272
 
273
+ type HostGuarantees = {
274
+ browserUrlVisibility?: true;
275
+ personConsent?: true;
276
+ };
277
+
273
278
  type Host = {
279
+ guarantees: HostGuarantees;
274
280
  report(): HostReport;
275
281
  subscribe(listener: (report: HostReport) => void): Unsubscribe;
276
282
  };
@@ -315,7 +321,7 @@ type CompleteAuthenticationResult =
315
321
  | { status: "rejected"; failure: AuthenticationFailure };
316
322
 
317
323
  type AuthenticationActionResult =
318
- | { status: "accepted" }
324
+ | { status: "applied" }
319
325
  | { status: "failed"; failure: AuthenticationFailure };
320
326
 
321
327
  type Unsubscribe = () => void;
@@ -374,7 +380,7 @@ type AgentCapacity = {
374
380
  };
375
381
 
376
382
  type CapacityResult =
377
- | { status: "accepted" }
383
+ | { status: "applied" }
378
384
  | { status: "failed"; failure: ProtocolFailure };
379
385
  ```
380
386
 
@@ -413,7 +419,7 @@ type DispositionCode = { id: string; label: string; group?: string };
413
419
 
414
420
  type DispositionRules = {
415
421
  required?: boolean;
416
- notes?: "required" | "optional" | "hidden";
422
+ notes?: "required" | "optional" | "none";
417
423
  codes?: DispositionCode[];
418
424
  };
419
425
 
@@ -609,8 +615,8 @@ type Task<C extends Channel = Channel> = {
609
615
 
610
616
  type AcceptanceMode =
611
617
  | "no-preference"
612
- | "require-agent-acceptance"
613
- | "require-automatic-acceptance";
618
+ | "consent"
619
+ | "automatic";
614
620
 
615
621
  type TaskOutcome =
616
622
  | { type: "completed"; by: "agent" | "provider" }
@@ -747,8 +753,8 @@ controls — is content, and is never locked.
747
753
 
748
754
  **A lead sets the team's policy from their roster.** A login that declares
749
755
  `capabilities.team.policyControl` may `executeTeamPolicy({ type: "set", capability, setting })`
750
- with `on`, `off`, or `agent`, for any task control, `dial`, or a skill — and only `hold`, `mute`
751
- and skills may be `agent`; callback and new call are the team's, on or off, within what the queue
756
+ with `on`, `off`, or `person`, for any task control, `dial`, or a skill — and only `hold`, `mute`
757
+ and skills may be `person`; callback and new call are the team's, on or off, within what the queue
752
758
  allows. The roster carries `policies` for such a login: every policy as it stands, who set it, and
753
759
  `lockedBy` where a level above the team made it theirs to keep, which the lead sees and cannot
754
760
  change — `executeTeamPolicy` on it answers `failed` with `omni.capability-not-enabled`.
@@ -856,7 +862,7 @@ type PolicyKey =
856
862
  | "dial"
857
863
  | `skill:${string}`;
858
864
 
859
- type TeamPolicySetting = "on" | "off" | "agent";
865
+ type TeamPolicySetting = "on" | "off" | "person";
860
866
 
861
867
  type TeamPolicy = Resolved & {
862
868
  setting: TeamPolicySetting;
@@ -1474,12 +1480,12 @@ idleCapabilities: {
1474
1480
  | `access.mode` | `allow-all` permits unmatched URLs; `block-all` denies unmatched URLs. |
1475
1481
  | `access.allowList` | URL-pattern exceptions permitted when the mode is `block-all`. |
1476
1482
  | `access.blockList` | Explicit denials. A match takes precedence over the same policy's allow list and mode. |
1477
- | `accessPolicyScope` | `all-navigation` by default: every redirect and navigation is checked. `initial-url` checks the starting URL alone. |
1483
+ | `accessAppliesTo` | `all-navigation` by default: every redirect and navigation is checked. `initial-url` checks the starting URL alone. |
1478
1484
 
1479
1485
  Patterns use the standard `URLPattern` syntax. Omni owns browser navigation, and the browser is
1480
1486
  hidden when no active provider contributes one.
1481
1487
 
1482
- `accessPolicyScope` defaults to `all-navigation`: every redirect and subsequent navigation is
1488
+ `accessAppliesTo` defaults to `all-navigation`: every redirect and subsequent navigation is
1483
1489
  validated against the current combined policy, not only the starting URL. A provider may set
1484
1490
  `initial-url` to check the first hop alone, but that has to be asked for. A `block-all` policy
1485
1491
  enforced only on the initial URL stops nothing — one redirect leaves it — so the permissive
@@ -1682,7 +1688,7 @@ them from what arrives later.
1682
1688
  | `team` | This login leads a team. The provider publishes a `TeamRoster` to it on every snapshot — `[]` when nobody is in it — and to nobody else. |
1683
1689
  | `team.breakControl` | This lead may act on their team's breaks through `executeTeamBreak` — place, release, decide, set policy — as far as the provider supports; a command it lacks answers `omni.capability-not-enabled`. Omni asks for a decision only against a member whose `break` is `awaiting-decision`, so a provider that grants on request is never asked to decide. Requires `executeTeamBreak`. |
1684
1690
  | `team.consultControl` | This lead may join a member's call on request. Requires `executeTeamConsult`. |
1685
- | `team.policyControl` | This lead sets the team's policy per capability — on, off, or the agent's — within what the queue allows. Requires `executeTeamPolicy`; the roster carries `policies`. |
1691
+ | `team.policyControl` | This lead sets the team's policy per capability — on, off, or the person's — within what the queue allows. Requires `executeTeamPolicy`; the roster carries `policies`. |
1686
1692
  | `preferences` | What the team left to this person, with where each stands and who set it. Omitted when nothing was. Requires `setPreference`. See **Who decides what an agent may do**. |
1687
1693
 
1688
1694
  A session action is available only when both the capability and Omni provisioning permit it.
@@ -1765,7 +1771,7 @@ settles. The adapter must not persist raw credentials. Field-specific failures m
1765
1771
 
1766
1772
  `cancelAuthentication(flowId)` cancels an abandoned Browser SSO window or credentials form and
1767
1773
  releases its temporary state. It does not sign out an already authenticated session. It answers
1768
- `accepted`; a repeat, or a flow that already ended, is nothing to act on and answers `accepted`
1774
+ `applied`; a repeat, or a flow that already ended, is nothing to act on and answers `applied`
1769
1775
  too, by the rule that a command asking for a state answers success when that state holds.
1770
1776
 
1771
1777
  ### Completion and failures
@@ -1979,24 +1985,27 @@ provider includes an acceptance directive with each allocation:
1979
1985
  | Directive | Contract |
1980
1986
  | --- | --- |
1981
1987
  | `no-preference` | The provider leaves acceptance to Omni; with `autoAcceptTasks: true`, Omni accepts automatically. |
1982
- | `require-agent-acceptance` | Omni presents **Accept** and waits for the agent. |
1983
- | `require-automatic-acceptance` | Omni accepts immediately without agent interaction. |
1988
+ | `consent` | The provider requires the person's explicit consent: Omni presents **Accept** and waits, whatever its own policy would have done. A host that declares `guarantees.personConsent` promises exactly this; a provider checks it before offering work only a person may take. |
1989
+ | `automatic` | Omni accepts immediately without agent interaction. |
1984
1990
 
1985
1991
  When Omni sent `autoAcceptTasks: false`, the provider omits `acceptanceMode` and every task
1986
- requires agent acceptance.
1992
+ requires agent acceptance. The two are never confused on the wire: `consent` is always the
1993
+ provider's requirement, stated on a wire where Omni was willing to accept for the agent; Omni's own
1994
+ no-auto-accept policy puts no word on the wire at all — the field is absent, and the **Accept**
1995
+ press is Omni's doing, not the provider's.
1987
1996
 
1988
1997
  **An absent value means `true`**, as `readyOnLogin` does, because an agent who has signed in and
1989
1998
  gone ready is telling the deployment they are working. Requiring a press before every contact is
1990
1999
  the exception a provisioning file asks for, not the state it falls into when a flag is missing.
1991
2000
 
1992
2001
  Nothing is given away by that default. `acceptanceMode` is the provider's own control and outranks
1993
- it: `require-agent-acceptance` puts the decision back in the agent's hands for any task where it
2002
+ it: `consent` puts the decision back in the agent's hands for any task where it
1994
2003
  belongs, whatever the host was configured with.
1995
2004
 
1996
2005
  An automatically accepted task still arrives through `task-offered`.
1997
2006
 
1998
2007
  Agent-initiated work arrives through `task-offered` with
1999
- `acceptanceMode: "require-automatic-acceptance"`.
2008
+ `acceptanceMode: "automatic"`.
2000
2009
 
2001
2010
  ### Pending
2002
2011
 
@@ -2011,7 +2020,7 @@ declare const task: Task;
2011
2020
  const allocation = {
2012
2021
  type: "task-offered",
2013
2022
  task,
2014
- acceptanceMode: "require-agent-acceptance",
2023
+ acceptanceMode: "consent",
2015
2024
  allocationExpiresAt: "2026-08-25T10:41:07.000Z",
2016
2025
  preparationEndsAt: "2026-08-25T10:40:37.000Z",
2017
2026
  } satisfies Extract<ProviderEvent, { type: "task-offered" }>;
@@ -2021,7 +2030,7 @@ The rule the phase exists to express: **nothing is acquired on the agent's behal
2021
2030
  is pending.** A host that carries media must not open the microphone until the task is
2022
2031
  accepted. Omni does not open the task's browsers either — a task that rings out costs nothing.
2023
2032
 
2024
- When manual acceptance is required, Omni offers the agent an **Accept** control. The call is the
2033
+ When consent is required, Omni offers the agent an **Accept** control. The call is the
2025
2034
  medium the task arrives on, not a separate decision.
2026
2035
 
2027
2036
  **Once a task is accepted, the call that comes with it is answered.** Omni has no discretion
@@ -2331,7 +2340,7 @@ Each `TaskBrowser` defines one named browser in the task workspace.
2331
2340
  | `url` | Initial URL. Must use `http:` or `https:`; see below. Later navigation comes from Chromium. |
2332
2341
  | `sharedSession` | Required. `false` creates a task-specific browser session. |
2333
2342
  | `isolationScheme` | **Required when `sharedSession` is `true`**, and rejected when it is `false`. There is no default: see below. |
2334
- | `urlVisibility` | What the agent sees of this tab's URL in Omni's chrome: `hidden`, `domain`, or `full`. Omitted, the URL shows as any browser's does; a provider says `hidden` where the URL carries what the agent may not read — a caller's number, a CRM token. Per browser, on the provider's word; Omni honours it tab by tab. |
2343
+ | `urlVisibility` | What the agent sees of this tab's URL in Omni's chrome: `hidden`, `domain`, or `full`. Omitted, the URL shows as any browser's does; a provider says `hidden` where the URL carries what the agent may not read — a caller's number, a CRM token. Per browser, on the provider's word; a host that declares `guarantees.browserUrlVisibility` honours it tab by tab, and a provider checks that guarantee before it sends such a URL at all. |
2335
2344
 
2336
2345
  ##### Choosing an isolation scheme
2337
2346
 
@@ -2464,7 +2473,7 @@ capabilities: {
2464
2473
  | Field | Contract |
2465
2474
  | --- | --- |
2466
2475
  | `required` | When `true`, Omni must collect a code before issuing `complete`. A required policy must publish at least one code. |
2467
- | `notes` | `required`, `optional`, or `hidden`; controls the free-text field beside the code. |
2476
+ | `notes` | `required`, `optional`, or `none`; controls the free-text field beside the code. |
2468
2477
  | `codes` | Codes Omni offers. `id` values are non-empty and unique; Omni sends the chosen `id` as `TaskCommand.complete.disposition`. |
2469
2478
 
2470
2479
  With `dispositions: true` Omni shows a Complete control and sends `complete` with no code, because
@@ -2693,7 +2702,7 @@ everywhere; success is not.
2693
2702
 
2694
2703
  | Method | Succeeded |
2695
2704
  | --- | --- |
2696
- | `setCapacity` | `accepted` |
2705
+ | `setCapacity` | `applied` |
2697
2706
  | `requestBreak` | `requested` |
2698
2707
  | `commitBreak` | `committed` |
2699
2708
  | `cancelBreak` | `cancelled` |
@@ -3080,7 +3089,7 @@ executeTeamConsult({ command: { type: "decline", requestId: "req-7", reason: "In
3080
3089
  ```
3081
3090
 
3082
3091
  **On `join` the provider bridges three parties and the lead is on a task of their own**, on the
3083
- same task id, arriving on the lead's connection as `task-offered` with `require-automatic-acceptance`
3092
+ same task id, arriving on the lead's connection as `task-offered` with `automatic`
3084
3093
  -- the way a call an agent placed themselves arrives -- and carrying `assisting`. The agent's task
3085
3094
  moves to `lead: { stage: "joined", leadId }`. A join is the lead's own act, so capacity does not
3086
3095
  trigger it; but from then on it is an outstanding task the provider counts against the lead's
@@ -3190,12 +3199,27 @@ the microphone once as the voice connection opens, so the permission prompt land
3190
3199
  is signing in rather than over a contact, prompts, retries on the agent's request, tells the agent
3191
3200
  what failed, and reports. It never decides for the adapter what a missing microphone means.
3192
3201
 
3202
+ **The host also guarantees, and a promise the provider cannot see is not one it can rely on.**
3203
+ Two of this contract's obligations fall on the host rather than the provider — honouring a task
3204
+ browser's `urlVisibility`, and taking a `consent` offer only on the person's own press — and more
3205
+ than one desk speaks this contract. So `ConnectContext.host.guarantees` says which promises the
3206
+ connected host makes, declared once per connection, presence being the guarantee exactly as it is
3207
+ the permission everywhere else:
3208
+
3209
+ | Guarantee | Contract |
3210
+ | --- | --- |
3211
+ | `browserUrlVisibility` | Every task browser's `urlVisibility` is honoured in this host's chrome, tab by tab. A provider that would send a caller's number in a URL checks this first and tokenises where the promise is absent. |
3212
+ | `personConsent` | A `consent` offer is accepted only by the person's own explicit act, never on their behalf. A provider whose work may only be taken by a human checks this first and does not offer it where the promise is absent. |
3213
+
3214
+ A guarantee the host does not make is an absent key, never `false` — `validateHostGuarantees`
3215
+ refuses a false one, as it refuses a name this contract does not list.
3216
+
3193
3217
  | Field | Contract |
3194
3218
  | --- | --- |
3195
3219
  | `online` | Whether the host has a network interface up. Not a claim that anything is reachable — the adapter knows whether it can reach its own platform far better than the host does — so `false` is a reason not to go ready and `true` is not a reason to. |
3196
3220
  | `audio` | Present on a voice connection, absent where there is no audio. |
3197
- | `audio.input` | `ready` with `localAudio` — the microphone as captured, the same stream `openMedia` receives — and `flowing`, false while the hardware or OS says no audio moves through it (a headset's own mute switch, which Omni's Mute control never touches). `unavailable` with `reason`, since each wants a different fix from the agent: `no-device`; `denied`; `not-asked`, which a host that asks at connect never publishes; `in-use`, a device present and permitted that another application holds — on an agent desktop the commonest of all; `lost`, a capture that ended. A host decides the reason from the devices before the error name: a browser can report a permission error on a machine with no microphone at all, and "grant permission" is the wrong instruction for an agent who needs to plug one in. `failure` carries the words Omni showed them. |
3198
- | `audio.output` | `ready`, or `unavailable` with `reason` — `no-device`, or `lost` for one removed — and `failure`: an agent who cannot hear is as unable to take a call as one who cannot speak. |
3221
+ | `audio.input` | `available` with `localAudio` — the microphone as captured, the same stream `openMedia` receives — and `flowing`, false while the hardware or OS says no audio moves through it (a headset's own mute switch, which Omni's Mute control never touches). `unavailable` with `reason`, since each wants a different fix from the agent: `no-device`; `denied`; `not-asked`, which a host that asks at connect never publishes; `in-use`, a device present and permitted that another application holds — on an agent desktop the commonest of all; `lost`, a capture that ended. A host decides the reason from the devices before the error name: a browser can report a permission error on a machine with no microphone at all, and "grant permission" is the wrong instruction for an agent who needs to plug one in. `failure` carries the words Omni showed them. |
3222
+ | `audio.output` | `available`, or `unavailable` with `reason` — `no-device`, or `lost` for one removed — and `failure`: an agent who cannot hear is as unable to take a call as one who cannot speak. |
3199
3223
 
3200
3224
  Omni republishes the report whenever it changes — a permission granted late, a headset unplugged,
3201
3225
  a network gone — and it publishes a state, not a flicker: a change that resolves within moments is
@@ -3647,7 +3671,8 @@ same exported checks are used by Omni and adapter tests so their interpretations
3647
3671
  | `validateEventEnvelope(envelope, manifest)` | Envelope identity, timestamp, and the payload for each event type. |
3648
3672
  | `validateContact(contact)` | Contact field shapes and attribute keys. Every field is optional, so this checks what is present rather than what is missing. |
3649
3673
  | `validateScheduledActivity(activity)` | Required activity fields and start/end ordering. |
3650
- | `validateHostReport(report)` | The host's own report as published to an adapter: `online`, and where there is audio, an input that is `ready` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `ready` or `unavailable` with its failure. The harness validates whatever host a test hands the adapter; `stillHost(report)` builds one that never changes. |
3674
+ | `validateHostGuarantees(guarantees)` | What a host promises: only the guarantees this contract names, each declared by presence and never `false`. The harness validates the guarantees of whatever host a test hands the adapter. |
3675
+ | `validateHostReport(report)` | The host's own report as published to an adapter: `online`, and where there is audio, an input that is `available` with the microphone and `flowing`, or `unavailable` with a reason and the failure that says why, and an output that is `available` or `unavailable` with its failure. The harness validates whatever host a test hands the adapter; `stillHost(report)` builds one that never changes. |
3651
3676
  | `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. |
3652
3677
  | `validateAuthenticationState(state)` | The identity each state must carry, the capabilities a usable login declares, and the expiry that only `authenticated` may. Omni applies it to every state a session publishes — the republished as much as the first. |
3653
3678
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xema/omni-protocol",
3
- "version": "0.1.27",
3
+ "version": "0.1.28",
4
4
  "description": "The Omni protocol: the contract every provider adapter implements",
5
5
  "type": "module",
6
6
  "license": "MIT",