@phosphorco/bb-identity 0.2.1 → 0.3.1

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
@@ -463,3 +463,21 @@ lookup decoding. Later conformance must use a packed artifact with two
463
463
  independently bundled UI consumers and real upstream→fork→upstream→fork
464
464
  sessions. Preserve native content, provider references and upstream grouped-edit
465
465
  restrictions in that proof.
466
+
467
+ ## External person attribution V1 (0.3)
468
+
469
+ `binding.server.externalContributionCapabilities()` synchronously distinguishes an absent capability (`ok(null)`) from a malformed or incompatible host (`ok: false`). A capable host advertises V1, canonical `person` identities, accepted/queued history, and `rollback: 'requires-capable-host'`. This optional subprotocol fits inside outer protocol 1; legacy `sendExternal` and `lookupOperation` retain their existing wire format and hashes.
470
+
471
+ For a new operation, `sendExternalContribution({version: 1, author, personAttribution}, input)` retains the external Slack/integration origin and an optional canonical person snapshot. The person snapshot contains `identity`, `presentation`, and `evidence: 'integration-asserted'`. It attributes a contribution in a trusted workspace; it conveys no authentication or authorization. Local `default-user` identities are not canonical people.
472
+
473
+ Persist the exact contribution, operation ID, prompt bytes, and selected transport before dispatch. Capability absence may select legacy external-only attribution for a **new** operation. Never upgrade, downgrade, or rebuild an existing operation from a current directory or mapping. A malformed capability is an incompatibility, not permission to fall back.
474
+
475
+ V1 receipts confirm the frozen `externalContribution`. Reconcile V1 exclusively through `lookupExternalContribution`, checking its returned attribution against the reservation. Wrong-operation receipts fail validation. Changed or malformed send receipts and exceptions after dispatch are indeterminate. Unknown/unsupported lookup does not authorize resend; only `absent-final` permits the same immutable operation to be retried. Legacy final receipts remain unchanged.
476
+
477
+ The default host rendering policy adds the integration sender envelope once. A producer such as Rosetta must select `externalMessageRendering: 'producer'` at binding construction; its prompt bytes remain unchanged. A capable native host projects the person into the native timeline while contribution/attempt history keeps the external origin and optional `personAttribution`. This does not add native-message history queries.
478
+
479
+ Native history containing integration-asserted person authors requires a capable runtime even after queues drain or plugins are downgraded. Do not roll back to a host that only accepts provider-verified person authors. Package-only rollback retains the legacy external path; it does not make old native readers understand newer stored authorship.
480
+
481
+ ### 0.3.1 decoder corrections
482
+
483
+ V1 external subjects are opaque nonempty strings of at most256 characters, including email-shaped or percent-encoded subjects. They are not BB resource IDs. Contribution and attempt-history output normalize additive person snapshot, identity, and presentation fields while preserving strict V1 input decoding. `externalContributionOutcomeV1Codec` from `/host` validates and normalizes persisted V1 outcomes; consumers must additionally compare submitted receipts with the reserved operation ID and frozen contribution. A stored final lacking that evidence remains uncertainty and must not trigger a resend.
package/bb.d.ts CHANGED
@@ -76,6 +76,9 @@ export interface BbInvocationServer {
76
76
  selfProfile(invocation: BbInvocation): Promise<Result<import('./model.js').IdentityProfile>>;
77
77
  personRequest(invocation: BbInvocation): Promise<Result<PersonRequest<BbPromptInput>>>;
78
78
  readonly sendExternal: IdentityServer<unknown, unknown, BbPromptInput>['sendExternal'];
79
+ readonly externalContributionCapabilities: IdentityServer<unknown, unknown, BbPromptInput>['externalContributionCapabilities'];
80
+ readonly sendExternalContribution: IdentityServer<unknown, unknown, BbPromptInput>['sendExternalContribution'];
81
+ readonly lookupExternalContribution: IdentityServer<unknown, unknown, BbPromptInput>['lookupExternalContribution'];
79
82
  readonly lookupOperation: IdentityServer<unknown, unknown, BbPromptInput>['lookupOperation'];
80
83
  /** Bounded, validated provenance reads; disposal fences new raw history calls. */
81
84
  readonly history: IdentityServer<unknown, unknown, BbPromptInput>['history'];
@@ -64,6 +64,29 @@ function retainIdentityHttpResponse(response, invocationSignal, requestSignal, r
64
64
  // bb-upstream-runtime.ts
65
65
  import { z } from "zod";
66
66
 
67
+ // external-person-output-runtime.ts
68
+ function record(value) {
69
+ return value !== null && typeof value === "object" && !Array.isArray(value);
70
+ }
71
+ function normalizeExternalPersonOutput(value) {
72
+ if (!record(value) || !record(value.identity) || !record(value.presentation))
73
+ return value;
74
+ return {
75
+ identity: { kind: value.identity.kind, key: value.identity.key, issuer: value.identity.issuer, subject: value.identity.subject },
76
+ presentation: { displayName: value.presentation.displayName, handle: value.presentation.handle, avatarUrl: value.presentation.avatarUrl },
77
+ evidence: value.evidence
78
+ };
79
+ }
80
+ function normalizeContributionOutput(value) {
81
+ return record(value) && value.personAttribution !== undefined ? { ...value, personAttribution: normalizeExternalPersonOutput(value.personAttribution) } : value;
82
+ }
83
+ function normalizeProvenanceOutput(value) {
84
+ return record(value) && Array.isArray(value.contributions) ? { ...value, contributions: value.contributions.map(normalizeContributionOutput) } : value;
85
+ }
86
+ function normalizeEvidencePageOutput(value, normalize) {
87
+ return record(value) && Array.isArray(value.items) ? { ...value, items: value.items.map(normalize) } : value;
88
+ }
89
+
67
90
  // model-runtime.ts
68
91
  var ID_MAX_LENGTH = 256;
69
92
  var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
@@ -172,7 +195,7 @@ function isOrigin(value) {
172
195
  return value.kind === "unknown" && (value.reason === "legacy" || value.reason === "upstream-unattributed" || value.reason === "missing-source");
173
196
  }
174
197
  function isContribution(value) {
175
- return isRecord(value) && isContributionReference(value.reference) && isOrigin(value.author) && (value.latestEditor === null || isActorSnapshot(value.latestEditor)) && isBoundedString(value.acceptedAt, 256) && Array.isArray(value.mentionedPeople) && value.mentionedPeople.every(isPersonReference);
198
+ return isRecord(value) && isContributionReference(value.reference) && isOrigin(value.author) && (value.personAttribution === undefined || externalPersonSnapshotCodec.decode(value.personAttribution).ok) && (value.latestEditor === null || isActorSnapshot(value.latestEditor)) && isBoundedString(value.acceptedAt, 256) && Array.isArray(value.mentionedPeople) && value.mentionedPeople.every(isPersonReference);
176
199
  }
177
200
  function isCorrelation(value) {
178
201
  return isRecord(value) && idCodec("thread").decode(value.threadId).ok && isBoundedString(value.turnId) && isBoundedString(value.attemptId) && (value.toolCallId === null || isBoundedString(value.toolCallId));
@@ -230,6 +253,7 @@ function hasCompleteCausality(groups, contributions, missing) {
230
253
  }
231
254
  var provenanceCodec = {
232
255
  decode(input) {
256
+ input = normalizeProvenanceOutput(input);
233
257
  if (!isRecord(input) || input.status !== "known" && input.status !== "partial" && input.status !== "unknown") {
234
258
  return err("invalid-input", "Invalid execution provenance.");
235
259
  }
@@ -249,6 +273,17 @@ var provenanceCodec = {
249
273
  return value;
250
274
  }
251
275
  };
276
+ var externalPersonSnapshotCodec = {
277
+ decode(value) {
278
+ if (!isRecord(value) || Object.keys(value).some((key) => !["identity", "presentation", "evidence"].includes(key)) || value.evidence !== "integration-asserted" || !isActorSnapshot(value) || !isRecord(value.identity) || value.identity.kind !== "person" || Object.keys(value.identity).some((key) => !["kind", "key", "issuer", "subject"].includes(key))) {
279
+ return err("invalid-input", "External person attribution requires a canonical person and integration-asserted evidence.");
280
+ }
281
+ return ok(value);
282
+ },
283
+ encode(value) {
284
+ return value;
285
+ }
286
+ };
252
287
  function defaultIdentityKey(instance) {
253
288
  return `local:${instance}`;
254
289
  }
@@ -396,39 +431,39 @@ function createBbUpstreamDriver(options) {
396
431
  const active = new Set;
397
432
  const listeners = new Set;
398
433
  const labelledExternal = new WeakSet;
399
- const terminateScope = (record, scope) => {
434
+ const terminateScope = (record2, scope) => {
400
435
  if (!scope || scope.released)
401
436
  return;
402
437
  scope.released = true;
403
- record.controller.signal.removeEventListener("abort", scope.parentAbort);
438
+ record2.controller.signal.removeEventListener("abort", scope.parentAbort);
404
439
  scope.controller.abort();
405
- record.scopes.delete(scope);
440
+ record2.scopes.delete(scope);
406
441
  };
407
- const terminate = (record) => {
408
- for (const scope of [...record.scopes])
409
- terminateScope(record, scope);
410
- record.controller.abort();
411
- active.delete(record);
442
+ const terminate = (record2) => {
443
+ for (const scope of [...record2.scopes])
444
+ terminateScope(record2, scope);
445
+ record2.controller.abort();
446
+ active.delete(record2);
412
447
  };
413
448
  const known = (context) => {
414
- const record = records.get(context);
415
- return !record ? err("invalid-input", "Invocation context was not issued by this binding.") : disposed ? err("disposed", "Upstream binding is disposed.") : record.controller.signal.aborted ? err("expired", "Invocation context has expired.") : ok(record);
449
+ const record2 = records.get(context);
450
+ return !record2 ? err("invalid-input", "Invocation context was not issued by this binding.") : disposed ? err("disposed", "Upstream binding is disposed.") : record2.controller.signal.aborted ? err("expired", "Invocation context has expired.") : ok(record2);
416
451
  };
417
452
  const admission = (context) => {
418
- const record = known(context);
419
- if (!record.ok)
420
- return record;
421
- if (record.value.origin === "external")
453
+ const record2 = known(context);
454
+ if (!record2.ok)
455
+ return record2;
456
+ if (record2.value.origin === "external")
422
457
  return err("unauthenticated", "External upstream work uses an explicit external source.");
423
- return record;
458
+ return record2;
424
459
  };
425
460
  const session = (context) => {
426
- const record = known(context);
427
- if (!record.ok)
428
- return unavailableSession(instanceId, record.error.code === "invalid-input" ? "incompatible" : "unauthenticated", record.error.message);
429
- if (record.value.origin === "interactive-user")
461
+ const record2 = known(context);
462
+ if (!record2.ok)
463
+ return unavailableSession(instanceId, record2.error.code === "invalid-input" ? "incompatible" : "unauthenticated", record2.error.message);
464
+ if (record2.value.origin === "interactive-user")
430
465
  return readySession();
431
- return record.value.origin === "background" ? machineSession() : unavailableSession(instanceId, "unauthenticated", "External upstream work uses an explicit external source.");
466
+ return record2.value.origin === "background" ? machineSession() : unavailableSession(instanceId, "unauthenticated", "External upstream work uses an explicit external source.");
432
467
  };
433
468
  const driver = {
434
469
  instanceId,
@@ -446,20 +481,20 @@ function createBbUpstreamDriver(options) {
446
481
  const allowed = admission(context);
447
482
  if (!allowed.ok)
448
483
  return allowed;
449
- const record = allowed.value;
484
+ const record2 = allowed.value;
450
485
  let scope;
451
- const parentAbort = () => terminateScope(record, scope);
486
+ const parentAbort = () => terminateScope(record2, scope);
452
487
  scope = { controller: new AbortController, parentAbort, released: false };
453
- record.scopes.add(scope);
454
- const expire = () => terminateScope(record, scope);
455
- record.controller.signal.addEventListener("abort", parentAbort, { once: true });
456
- const exposedSession = record.origin === "background" ? machineSession() : readySession();
457
- const expectedKey = record.origin === "background" ? machineKey.value : singletonKey;
488
+ record2.scopes.add(scope);
489
+ const expire = () => terminateScope(record2, scope);
490
+ record2.controller.signal.addEventListener("abort", parentAbort, { once: true });
491
+ const exposedSession = record2.origin === "background" ? machineSession() : readySession();
492
+ const expectedKey = record2.origin === "background" ? machineKey.value : singletonKey;
458
493
  return ok({
459
494
  signal: scope.controller.signal,
460
495
  session: exposedSession,
461
496
  validate(expected) {
462
- if (scope.released || scope.controller.signal.aborted || record.controller.signal.aborted || disposed) {
497
+ if (scope.released || scope.controller.signal.aborted || record2.controller.signal.aborted || disposed) {
463
498
  return err("expired", "Upstream request context is no longer live.");
464
499
  }
465
500
  return expected.actor === expectedKey && expected.session === stamp.value ? ok(undefined) : err("stale-context", "Request expectation does not match the singleton session.");
@@ -543,22 +578,22 @@ function createBbUpstreamDriver(options) {
543
578
  throw new Error("Upstream binding is disposed.");
544
579
  const controller = new AbortController;
545
580
  const invocation = Object.freeze({ origin, signal: controller.signal });
546
- const record = { origin, controller, scopes: new Set };
547
- records.set(invocation, record);
548
- active.add(record);
581
+ const record2 = { origin, controller, scopes: new Set };
582
+ records.set(invocation, record2);
583
+ active.add(record2);
549
584
  return invocation;
550
585
  },
551
586
  release(invocation) {
552
- const record = records.get(invocation);
553
- if (record)
554
- terminate(record);
587
+ const record2 = records.get(invocation);
588
+ if (record2)
589
+ terminate(record2);
555
590
  },
556
591
  dispose() {
557
592
  if (disposed)
558
593
  return;
559
594
  disposed = true;
560
- for (const record of [...active])
561
- terminate(record);
595
+ for (const record2 of [...active])
596
+ terminate(record2);
562
597
  for (const listener of [...listeners]) {
563
598
  try {
564
599
  listener({ kind: "disposed" });
@@ -766,7 +801,7 @@ function isContribution2(value) {
766
801
  if (!isRecord3(origin))
767
802
  return false;
768
803
  const validOrigin = origin.kind === "person" ? snapshot(origin.actor) && isRecord3(origin.actor) && isPersonIdentity(origin.actor.identity) : origin.kind === "machine" ? snapshot(origin.actor) && isRecord3(origin.actor) && isMachineIdentity(origin.actor.identity) : origin.kind === "external" ? snapshot(origin.actor) && isRecord3(origin.actor) && isExternalIdentity(origin.actor.identity) : origin.kind === "agent" ? origin.agentId === null || idCodec("agent").decode(origin.agentId).ok : origin.kind === "system" ? typeof origin.reason === "string" && origin.reason.length > 0 && origin.reason.length <= 1024 : origin.kind === "unknown" && (origin.reason === "legacy" || origin.reason === "upstream-unattributed" || origin.reason === "missing-source");
769
- return validOrigin && (value.latestEditor === null || snapshot(value.latestEditor));
804
+ return (value.personAttribution === undefined || externalPersonSnapshotCodec.decode(value.personAttribution).ok) && validOrigin && (value.latestEditor === null || snapshot(value.latestEditor));
770
805
  }
771
806
  function decodeEvidencePage(value, decodeItem) {
772
807
  if (!isRecord3(value) || typeof value.status !== "string")
@@ -847,11 +882,11 @@ function createEnhancedDirectory(protocol) {
847
882
  }
848
883
  mappings.set(key, mapping);
849
884
  };
850
- const profile = (source, record, revision) => {
851
- if (!isRecord3(record) || !source.issuers.includes(record.issuer) || !idCodec("issuer").decode(record.issuer).ok || !idCodec("subject").decode(record.subject).ok || !isProfilePresentation(record.presentation) || record.status !== "current" && record.status !== "historical" || !idCodec("revision").decode(revision).ok)
885
+ const profile = (source, record2, revision) => {
886
+ if (!isRecord3(record2) || !source.issuers.includes(record2.issuer) || !idCodec("issuer").decode(record2.issuer).ok || !idCodec("subject").decode(record2.subject).ok || !isProfilePresentation(record2.presentation) || record2.status !== "current" && record2.status !== "historical" || !idCodec("revision").decode(revision).ok)
852
887
  return err("incompatible", "Provider directory record is malformed.");
853
- const issuer = record.issuer;
854
- const subject = record.subject;
888
+ const issuer = record2.issuer;
889
+ const subject = record2.subject;
855
890
  if (typeof issuer !== "string" || typeof subject !== "string")
856
891
  return err("incompatible", "Provider directory record is malformed.");
857
892
  let person;
@@ -862,10 +897,10 @@ function createEnhancedDirectory(protocol) {
862
897
  }
863
898
  if (!person.ok)
864
899
  return person;
865
- if (person.value.kind !== "person" || person.value.issuer !== record.issuer || person.value.subject !== record.subject || !identityKeyCodec.decode(person.value.key).ok) {
900
+ if (person.value.kind !== "person" || person.value.issuer !== record2.issuer || person.value.subject !== record2.subject || !identityKeyCodec.decode(person.value.key).ok) {
866
901
  return err("incompatible", "Provider directory returned an invalid person reference.");
867
902
  }
868
- return profileCodec.decode({ identity: person.value, presentation: record.presentation, revision, status: record.status });
903
+ return profileCodec.decode({ identity: person.value, presentation: record2.presentation, revision, status: record2.status });
869
904
  };
870
905
  return {
871
906
  async search(input, options) {
@@ -891,8 +926,8 @@ function createEnhancedDirectory(protocol) {
891
926
  if (!Array.isArray(result.value.records) || result.value.records.length > input.limit || result.value.nextCursor !== null && !idCodec("cursor").decode(result.value.nextCursor).ok || !idCodec("revision").decode(result.value.revision).ok)
892
927
  return err("incompatible", "Provider directory page is malformed.");
893
928
  const profiles = [];
894
- for (const record of result.value.records) {
895
- const decoded = profile(current.value, record, result.value.revision);
929
+ for (const record2 of result.value.records) {
930
+ const decoded = profile(current.value, record2, result.value.revision);
896
931
  if (!decoded.ok)
897
932
  return decoded;
898
933
  if (input.history === "current" && decoded.value.status !== "current")
@@ -1101,6 +1136,7 @@ function createHostAdapter(options) {
1101
1136
  });
1102
1137
  const contexts = new WeakMap;
1103
1138
  return {
1139
+ ...externalContributionMethods(upstream, extension, () => disposed),
1104
1140
  pluginId: upstream.pluginId,
1105
1141
  instanceId: upstream.instanceId,
1106
1142
  session: (context) => upstream.session(context),
@@ -1146,6 +1182,7 @@ function createHostAdapter(options) {
1146
1182
  const error = protocolError(extension);
1147
1183
  const unavailableResult = async () => ({ ok: false, error });
1148
1184
  return {
1185
+ ...externalContributionMethods(upstream, extension, () => disposed),
1149
1186
  pluginId: upstream.pluginId,
1150
1187
  instanceId: upstream.instanceId,
1151
1188
  session: async () => ({ status: error.code === "incompatible" ? "incompatible" : "unavailable", instanceId: upstream.instanceId, error }),
@@ -1199,7 +1236,7 @@ function createHostAdapter(options) {
1199
1236
  } catch {
1200
1237
  return err("unavailable", "Contribution history is unavailable.", "after-reconnect");
1201
1238
  }
1202
- return result.ok ? decodeEvidencePage(result.value, isContribution2) : result;
1239
+ return result.ok ? decodeEvidencePage(normalizeEvidencePageOutput(result.value, normalizeContributionOutput), isContribution2) : result;
1203
1240
  },
1204
1241
  async attempts(query, readOptions) {
1205
1242
  if (!validAttemptQuery(query))
@@ -1212,7 +1249,7 @@ function createHostAdapter(options) {
1212
1249
  } catch {
1213
1250
  return err("unavailable", "Execution history is unavailable.", "after-reconnect");
1214
1251
  }
1215
- return result.ok ? decodeEvidencePage(result.value, (item) => provenanceCodec.decode(item).ok) : result;
1252
+ return result.ok ? decodeEvidencePage(normalizeEvidencePageOutput(result.value, normalizeProvenanceOutput), (item) => provenanceCodec.decode(item).ok) : result;
1216
1253
  }
1217
1254
  };
1218
1255
  const subscription = protocol.subscribe((event) => {
@@ -1221,6 +1258,7 @@ function createHostAdapter(options) {
1221
1258
  });
1222
1259
  const scopes = new WeakMap;
1223
1260
  return {
1261
+ ...externalContributionMethods(upstream, extension, () => disposed),
1224
1262
  pluginId: upstream.pluginId,
1225
1263
  instanceId: protocol.instanceId,
1226
1264
  async session(context) {
@@ -1307,6 +1345,119 @@ function createHostAdapter(options) {
1307
1345
  }
1308
1346
  };
1309
1347
  }
1348
+ function decodeExternalContribution(value, strict = true) {
1349
+ if (!isRecord3(value) || value.version !== 1 || !isRecord3(value.author) || strict && Object.keys(value).some((key) => !["version", "author", "personAttribution"].includes(key)) || strict && Object.keys(value.author).some((key) => !["subject", "presentation"].includes(key)) || strict && isRecord3(value.author.presentation) && Object.keys(value.author.presentation).some((key) => !["displayName", "handle", "avatarUrl"].includes(key)) || (typeof value.author.subject !== "string" || value.author.subject.length === 0 || value.author.subject.length > 256) || !isProfilePresentation(value.author.presentation)) {
1350
+ return err("invalid-input", "Invalid V1 external contribution.");
1351
+ }
1352
+ let person = value.personAttribution;
1353
+ if (!strict)
1354
+ person = normalizeExternalPersonOutput(person);
1355
+ if (strict && isRecord3(person) && isRecord3(person.presentation) && Object.keys(person.presentation).some((key) => !["displayName", "handle", "avatarUrl"].includes(key)))
1356
+ return err("invalid-input", "Unexpected presentation field.");
1357
+ const decoded = person === undefined ? null : externalPersonSnapshotCodec.decode(person);
1358
+ if (decoded && !decoded.ok)
1359
+ return decoded;
1360
+ return ok({
1361
+ version: 1,
1362
+ author: { subject: value.author.subject, presentation: { displayName: value.author.presentation.displayName, handle: value.author.presentation.handle, avatarUrl: value.author.presentation.avatarUrl } },
1363
+ ...decoded?.ok ? { personAttribution: decoded.value } : {}
1364
+ });
1365
+ }
1366
+ var externalContributionV1Codec = {
1367
+ decode: (value) => decodeExternalContribution(value),
1368
+ encode: (value) => value
1369
+ };
1370
+ function canonicalExternal(value) {
1371
+ if (Array.isArray(value))
1372
+ return `[${value.map(canonicalExternal).join(",")}]`;
1373
+ if (isRecord3(value))
1374
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalExternal(value[key])}`).join(",")}}`;
1375
+ return JSON.stringify(value);
1376
+ }
1377
+ function decodeExternalContributionOutcomeV1(raw) {
1378
+ const decoded = decodeAcceptance(raw);
1379
+ if (!decoded.ok || decoded.value.status !== "submitted")
1380
+ return decoded;
1381
+ if (decoded.value.receipt.evidence !== "host-accepted")
1382
+ return err("incompatible", "V1 needs durable native acceptance.");
1383
+ const receipt = decoded.value.receipt;
1384
+ const contribution = decodeExternalContribution(receipt.externalContribution, false);
1385
+ return contribution.ok ? ok({ status: "submitted", receipt: { ...decoded.value.receipt, externalContribution: contribution.value } }) : err("incompatible", "Host receipt omitted or changed V1 attribution.");
1386
+ }
1387
+ function externalContributionMethods(upstream, extension, disposed) {
1388
+ const support = () => {
1389
+ if (disposed())
1390
+ return err("disposed", "Identity host is disposed.");
1391
+ if (extension.status === "absent")
1392
+ return ok(null);
1393
+ if (extension.status !== "supported")
1394
+ return { ok: false, error: protocolError(extension) };
1395
+ const value = extension.protocol.externalContributions;
1396
+ if (value === undefined)
1397
+ return ok(null);
1398
+ if (!isRecord3(value) || !Array.isArray(value.versions) || !value.versions.length || !value.versions.every((version) => Number.isInteger(version) && version > 0))
1399
+ return err("incompatible", "Malformed external contribution capability.");
1400
+ if (!value.versions.includes(1))
1401
+ return ok(null);
1402
+ if (!Array.isArray(value.personKinds) || value.personKinds.length !== 1 || value.personKinds[0] !== "person" || value.history !== "accepted-and-queued" || value.rollback !== "requires-capable-host" || typeof value.acceptV1 !== "function" || typeof value.lookupV1 !== "function")
1403
+ return err("incompatible", "Malformed V1 external contribution capability.");
1404
+ return ok(value);
1405
+ };
1406
+ return {
1407
+ externalContributionCapabilities() {
1408
+ const negotiated = support();
1409
+ return !negotiated.ok ? negotiated : ok(negotiated.value ? { version: 1, personKinds: ["person"], history: "accepted-and-queued", rollback: "requires-capable-host" } : null);
1410
+ },
1411
+ async sendExternalContribution(contribution, input, options) {
1412
+ const operationId = input.operationId;
1413
+ const decoded = decodeExternalContribution(contribution);
1414
+ if (!decoded.ok)
1415
+ return { status: "rejected", error: decoded.error };
1416
+ const negotiated = support();
1417
+ if (!negotiated.ok)
1418
+ return { status: "rejected", error: negotiated.error };
1419
+ if (!negotiated.value)
1420
+ return { status: "rejected", error: { code: "unsupported", message: "Native external-person attribution V1 is unavailable.", retry: "never" } };
1421
+ if (options?.signal?.aborted)
1422
+ return { status: "rejected", error: { code: "expired", message: "The send was cancelled before dispatch.", retry: "never" } };
1423
+ const frozen = JSON.parse(JSON.stringify(decoded.value));
1424
+ const wire = JSON.parse(JSON.stringify(wireInput(input, upstream.inputCodec)));
1425
+ try {
1426
+ const result = decodeExternalContributionOutcomeV1(await negotiated.value.acceptV1({ contribution: frozen, input: wire }, options));
1427
+ if (!result.ok)
1428
+ return { status: "indeterminate", operationId, message: result.error.message };
1429
+ if (result.value.status === "submitted" && (result.value.receipt.operationId !== operationId || canonicalExternal(result.value.receipt.externalContribution) !== canonicalExternal(frozen))) {
1430
+ return { status: "indeterminate", operationId, message: "Host receipt did not confirm the reserved attribution." };
1431
+ }
1432
+ return result.value;
1433
+ } catch {
1434
+ return { status: "indeterminate", operationId, message: "External contribution dispatch outcome is unknown; reconcile this operation." };
1435
+ }
1436
+ },
1437
+ async lookupExternalContribution(operationId, options) {
1438
+ const negotiated = support();
1439
+ if (!negotiated.ok)
1440
+ return negotiated;
1441
+ if (!negotiated.value)
1442
+ return ok({ status: "unknown", reason: "unsupported" });
1443
+ try {
1444
+ const result = asResult(await negotiated.value.lookupV1(operationId, options));
1445
+ if (!result.ok)
1446
+ return result;
1447
+ if (isRecord3(result.value) && result.value.status === "final") {
1448
+ const decoded = decodeExternalContributionOutcomeV1(result.value.outcome);
1449
+ if (decoded.ok && decoded.value.status === "submitted" && decoded.value.receipt.operationId !== operationId) {
1450
+ return err("incompatible", "Host lookup returned a receipt for another operation.");
1451
+ }
1452
+ return decoded.ok ? ok({ status: "final", outcome: decoded.value }) : decoded;
1453
+ }
1454
+ return decodeLookup(result.value);
1455
+ } catch {
1456
+ return err("unavailable", "External contribution lookup is unavailable.", "after-reconnect");
1457
+ }
1458
+ }
1459
+ };
1460
+ }
1310
1461
  function inspectForkExtension(value) {
1311
1462
  if (value === undefined)
1312
1463
  return { status: "absent" };
@@ -1464,6 +1615,9 @@ function createIdentityServer(options) {
1464
1615
  },
1465
1616
  toolProvenance: (context) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.readToolProvenance(context),
1466
1617
  sendExternal: (author, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternal(author, input, readOptions),
1618
+ externalContributionCapabilities: () => disposed ? err("disposed", "Identity server is disposed.") : host.externalContributionCapabilities(),
1619
+ sendExternalContribution: (contribution, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternalContribution(contribution, input, readOptions),
1620
+ lookupExternalContribution: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupExternalContribution(operationId, readOptions),
1467
1621
  lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupOperation(operationId, readOptions),
1468
1622
  dispose
1469
1623
  };
@@ -2283,8 +2437,8 @@ function createBbIdentityServerBinding(options) {
2283
2437
  const invocations = new WeakMap;
2284
2438
  const expired = () => err("expired", "Invocation has already settled.");
2285
2439
  const recordFor = (invocation) => {
2286
- const record = invocations.get(invocation);
2287
- return disposed || !record || record.settled || record.signal.aborted ? expired() : ok(record);
2440
+ const record2 = invocations.get(invocation);
2441
+ return disposed || !record2 || record2.settled || record2.signal.aborted ? expired() : ok(record2);
2288
2442
  };
2289
2443
  const unavailableSession2 = () => ({
2290
2444
  status: "unavailable",
@@ -2292,25 +2446,25 @@ function createBbIdentityServerBinding(options) {
2292
2446
  error: { code: "expired", message: "Invocation has already settled.", retry: "after-refresh" }
2293
2447
  });
2294
2448
  const openPerson = async (invocation) => {
2295
- const record = recordFor(invocation);
2296
- if (!record.ok)
2297
- return record;
2298
- const opened = await server.personRequest(record.value.request);
2299
- if (record.value.settled || record.value.signal.aborted) {
2449
+ const record2 = recordFor(invocation);
2450
+ if (!record2.ok)
2451
+ return record2;
2452
+ const opened = await server.personRequest(record2.value.request);
2453
+ if (record2.value.settled || record2.value.signal.aborted) {
2300
2454
  if (opened.ok)
2301
2455
  opened.value.dispose();
2302
2456
  return expired();
2303
2457
  }
2304
2458
  if (opened.ok)
2305
- record.value.people.add(opened.value);
2459
+ record2.value.people.add(opened.value);
2306
2460
  return opened;
2307
2461
  };
2308
2462
  const guardEndpoint = async (invocation, operation) => {
2309
- const record = recordFor(invocation);
2310
- if (!record.ok)
2311
- return record;
2312
- const result = await operation(record.value.request);
2313
- return record.value.settled || record.value.signal.aborted ? expired() : result;
2463
+ const record2 = recordFor(invocation);
2464
+ if (!record2.ok)
2465
+ return record2;
2466
+ const result = await operation(record2.value.request);
2467
+ return record2.value.settled || record2.value.signal.aborted ? expired() : result;
2314
2468
  };
2315
2469
  let disposeBinding = () => {};
2316
2470
  const guardedHistory = {
@@ -2331,11 +2485,11 @@ function createBbIdentityServerBinding(options) {
2331
2485
  instanceId: server.instanceId,
2332
2486
  commits: server.commits,
2333
2487
  async session(invocation) {
2334
- const record = recordFor(invocation);
2335
- if (!record.ok)
2488
+ const record2 = recordFor(invocation);
2489
+ if (!record2.ok)
2336
2490
  return unavailableSession2();
2337
- const session = await server.session(record.value.request);
2338
- return record.value.settled || record.value.signal.aborted ? unavailableSession2() : session;
2491
+ const session = await server.session(record2.value.request);
2492
+ return record2.value.settled || record2.value.signal.aborted ? unavailableSession2() : session;
2339
2493
  },
2340
2494
  selfProfile: (invocation) => guardEndpoint(invocation, (request) => server.selfProfile(request)),
2341
2495
  personRequest: openPerson,
@@ -2345,6 +2499,17 @@ function createBbIdentityServerBinding(options) {
2345
2499
  const renderedInput = upstream.value.externalMessageRendering === "host" ? renderExternalPrompt(upstream.value.driver.pluginId, author, input.input) : input.input;
2346
2500
  return server.sendExternal(author, { ...input, input: renderedInput }, readOptions);
2347
2501
  },
2502
+ externalContributionCapabilities: () => disposed ? err("disposed", "Identity binding is disposed.") : server.externalContributionCapabilities(),
2503
+ async sendExternalContribution(contribution, input, readOptions) {
2504
+ if (disposed)
2505
+ return { status: "rejected", error: { code: "disposed", message: "Identity binding is disposed.", retry: "never" } };
2506
+ const decoded = externalContributionV1Codec.decode(contribution);
2507
+ if (!decoded.ok)
2508
+ return { status: "rejected", error: decoded.error };
2509
+ const renderedInput = upstream.value.externalMessageRendering === "host" ? renderExternalPrompt(upstream.value.driver.pluginId, decoded.value.author, input.input) : input.input;
2510
+ return server.sendExternalContribution(decoded.value, { ...input, input: renderedInput }, readOptions);
2511
+ },
2512
+ lookupExternalContribution: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.lookupExternalContribution(operationId, readOptions),
2348
2513
  lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.lookupOperation(operationId, readOptions),
2349
2514
  history: guardedHistory
2350
2515
  };
@@ -2401,18 +2566,18 @@ function createBbIdentityServerBinding(options) {
2401
2566
  return endpointRegistered;
2402
2567
  }
2403
2568
  const createInvocation = (context) => {
2404
- const record = { request: context.request, signal: context.signal, people: new Set, settled: false };
2569
+ const record2 = { request: context.request, signal: context.signal, people: new Set, settled: false };
2405
2570
  const invocation = {
2406
2571
  origin: context.origin,
2407
2572
  signal: context.signal,
2408
2573
  person: () => context.origin === "external" ? Promise.resolve(err("unauthenticated", "External invocation uses an explicit external source.")) : openPerson(invocation)
2409
2574
  };
2410
- invocations.set(invocation, record);
2575
+ invocations.set(invocation, record2);
2411
2576
  const finish = () => {
2412
- record.settled = true;
2413
- for (const request of record.people)
2577
+ record2.settled = true;
2578
+ for (const request of record2.people)
2414
2579
  request.dispose();
2415
- record.people.clear();
2580
+ record2.people.clear();
2416
2581
  };
2417
2582
  return { invocation, finish };
2418
2583
  };