@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 +18 -0
- package/bb.d.ts +3 -0
- package/dist/bb-entry-runtime.js +235 -70
- package/dist/host-runtime.js +169 -12
- package/dist/index-runtime.js +33 -1
- package/dist/model-runtime.js +34 -1
- package/dist/server-runtime.js +3 -0
- package/dist/testing-runtime.js +240 -75
- package/host.d.ts +29 -0
- package/model.d.ts +7 -0
- package/package.json +1 -1
- package/server.d.ts +5 -1
package/dist/testing-runtime.js
CHANGED
|
@@ -1,4 +1,27 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// external-person-output-runtime.ts
|
|
3
|
+
function record(value) {
|
|
4
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5
|
+
}
|
|
6
|
+
function normalizeExternalPersonOutput(value) {
|
|
7
|
+
if (!record(value) || !record(value.identity) || !record(value.presentation))
|
|
8
|
+
return value;
|
|
9
|
+
return {
|
|
10
|
+
identity: { kind: value.identity.kind, key: value.identity.key, issuer: value.identity.issuer, subject: value.identity.subject },
|
|
11
|
+
presentation: { displayName: value.presentation.displayName, handle: value.presentation.handle, avatarUrl: value.presentation.avatarUrl },
|
|
12
|
+
evidence: value.evidence
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
function normalizeContributionOutput(value) {
|
|
16
|
+
return record(value) && value.personAttribution !== undefined ? { ...value, personAttribution: normalizeExternalPersonOutput(value.personAttribution) } : value;
|
|
17
|
+
}
|
|
18
|
+
function normalizeProvenanceOutput(value) {
|
|
19
|
+
return record(value) && Array.isArray(value.contributions) ? { ...value, contributions: value.contributions.map(normalizeContributionOutput) } : value;
|
|
20
|
+
}
|
|
21
|
+
function normalizeEvidencePageOutput(value, normalize) {
|
|
22
|
+
return record(value) && Array.isArray(value.items) ? { ...value, items: value.items.map(normalize) } : value;
|
|
23
|
+
}
|
|
24
|
+
|
|
2
25
|
// model-runtime.ts
|
|
3
26
|
var ID_MAX_LENGTH = 256;
|
|
4
27
|
var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/-]*$/;
|
|
@@ -107,7 +130,7 @@ function isOrigin(value) {
|
|
|
107
130
|
return value.kind === "unknown" && (value.reason === "legacy" || value.reason === "upstream-unattributed" || value.reason === "missing-source");
|
|
108
131
|
}
|
|
109
132
|
function isContribution(value) {
|
|
110
|
-
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);
|
|
133
|
+
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);
|
|
111
134
|
}
|
|
112
135
|
function isCorrelation(value) {
|
|
113
136
|
return isRecord(value) && idCodec("thread").decode(value.threadId).ok && isBoundedString(value.turnId) && isBoundedString(value.attemptId) && (value.toolCallId === null || isBoundedString(value.toolCallId));
|
|
@@ -165,6 +188,7 @@ function hasCompleteCausality(groups, contributions, missing) {
|
|
|
165
188
|
}
|
|
166
189
|
var provenanceCodec = {
|
|
167
190
|
decode(input) {
|
|
191
|
+
input = normalizeProvenanceOutput(input);
|
|
168
192
|
if (!isRecord(input) || input.status !== "known" && input.status !== "partial" && input.status !== "unknown") {
|
|
169
193
|
return err("invalid-input", "Invalid execution provenance.");
|
|
170
194
|
}
|
|
@@ -184,6 +208,17 @@ var provenanceCodec = {
|
|
|
184
208
|
return value;
|
|
185
209
|
}
|
|
186
210
|
};
|
|
211
|
+
var externalPersonSnapshotCodec = {
|
|
212
|
+
decode(value) {
|
|
213
|
+
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))) {
|
|
214
|
+
return err("invalid-input", "External person attribution requires a canonical person and integration-asserted evidence.");
|
|
215
|
+
}
|
|
216
|
+
return ok(value);
|
|
217
|
+
},
|
|
218
|
+
encode(value) {
|
|
219
|
+
return value;
|
|
220
|
+
}
|
|
221
|
+
};
|
|
187
222
|
function defaultIdentityKey(instance) {
|
|
188
223
|
return `local:${instance}`;
|
|
189
224
|
}
|
|
@@ -378,7 +413,7 @@ function isContribution2(value) {
|
|
|
378
413
|
if (!isRecord2(origin))
|
|
379
414
|
return false;
|
|
380
415
|
const validOrigin = origin.kind === "person" ? snapshot(origin.actor) && isRecord2(origin.actor) && isPersonIdentity(origin.actor.identity) : origin.kind === "machine" ? snapshot(origin.actor) && isRecord2(origin.actor) && isMachineIdentity(origin.actor.identity) : origin.kind === "external" ? snapshot(origin.actor) && isRecord2(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");
|
|
381
|
-
return validOrigin && (value.latestEditor === null || snapshot(value.latestEditor));
|
|
416
|
+
return (value.personAttribution === undefined || externalPersonSnapshotCodec.decode(value.personAttribution).ok) && validOrigin && (value.latestEditor === null || snapshot(value.latestEditor));
|
|
382
417
|
}
|
|
383
418
|
function decodeEvidencePage(value, decodeItem) {
|
|
384
419
|
if (!isRecord2(value) || typeof value.status !== "string")
|
|
@@ -459,11 +494,11 @@ function createEnhancedDirectory(protocol) {
|
|
|
459
494
|
}
|
|
460
495
|
mappings.set(key, mapping);
|
|
461
496
|
};
|
|
462
|
-
const profile = (source,
|
|
463
|
-
if (!isRecord2(
|
|
497
|
+
const profile = (source, record2, revision) => {
|
|
498
|
+
if (!isRecord2(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)
|
|
464
499
|
return err("incompatible", "Provider directory record is malformed.");
|
|
465
|
-
const issuer =
|
|
466
|
-
const subject =
|
|
500
|
+
const issuer = record2.issuer;
|
|
501
|
+
const subject = record2.subject;
|
|
467
502
|
if (typeof issuer !== "string" || typeof subject !== "string")
|
|
468
503
|
return err("incompatible", "Provider directory record is malformed.");
|
|
469
504
|
let person;
|
|
@@ -474,10 +509,10 @@ function createEnhancedDirectory(protocol) {
|
|
|
474
509
|
}
|
|
475
510
|
if (!person.ok)
|
|
476
511
|
return person;
|
|
477
|
-
if (person.value.kind !== "person" || person.value.issuer !==
|
|
512
|
+
if (person.value.kind !== "person" || person.value.issuer !== record2.issuer || person.value.subject !== record2.subject || !identityKeyCodec.decode(person.value.key).ok) {
|
|
478
513
|
return err("incompatible", "Provider directory returned an invalid person reference.");
|
|
479
514
|
}
|
|
480
|
-
return profileCodec.decode({ identity: person.value, presentation:
|
|
515
|
+
return profileCodec.decode({ identity: person.value, presentation: record2.presentation, revision, status: record2.status });
|
|
481
516
|
};
|
|
482
517
|
return {
|
|
483
518
|
async search(input, options) {
|
|
@@ -503,8 +538,8 @@ function createEnhancedDirectory(protocol) {
|
|
|
503
538
|
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)
|
|
504
539
|
return err("incompatible", "Provider directory page is malformed.");
|
|
505
540
|
const profiles = [];
|
|
506
|
-
for (const
|
|
507
|
-
const decoded = profile(current.value,
|
|
541
|
+
for (const record2 of result.value.records) {
|
|
542
|
+
const decoded = profile(current.value, record2, result.value.revision);
|
|
508
543
|
if (!decoded.ok)
|
|
509
544
|
return decoded;
|
|
510
545
|
if (input.history === "current" && decoded.value.status !== "current")
|
|
@@ -713,6 +748,7 @@ function createHostAdapter(options) {
|
|
|
713
748
|
});
|
|
714
749
|
const contexts = new WeakMap;
|
|
715
750
|
return {
|
|
751
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
716
752
|
pluginId: upstream.pluginId,
|
|
717
753
|
instanceId: upstream.instanceId,
|
|
718
754
|
session: (context) => upstream.session(context),
|
|
@@ -758,6 +794,7 @@ function createHostAdapter(options) {
|
|
|
758
794
|
const error = protocolError(extension);
|
|
759
795
|
const unavailableResult = async () => ({ ok: false, error });
|
|
760
796
|
return {
|
|
797
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
761
798
|
pluginId: upstream.pluginId,
|
|
762
799
|
instanceId: upstream.instanceId,
|
|
763
800
|
session: async () => ({ status: error.code === "incompatible" ? "incompatible" : "unavailable", instanceId: upstream.instanceId, error }),
|
|
@@ -811,7 +848,7 @@ function createHostAdapter(options) {
|
|
|
811
848
|
} catch {
|
|
812
849
|
return err("unavailable", "Contribution history is unavailable.", "after-reconnect");
|
|
813
850
|
}
|
|
814
|
-
return result.ok ? decodeEvidencePage(result.value, isContribution2) : result;
|
|
851
|
+
return result.ok ? decodeEvidencePage(normalizeEvidencePageOutput(result.value, normalizeContributionOutput), isContribution2) : result;
|
|
815
852
|
},
|
|
816
853
|
async attempts(query, readOptions) {
|
|
817
854
|
if (!validAttemptQuery(query))
|
|
@@ -824,7 +861,7 @@ function createHostAdapter(options) {
|
|
|
824
861
|
} catch {
|
|
825
862
|
return err("unavailable", "Execution history is unavailable.", "after-reconnect");
|
|
826
863
|
}
|
|
827
|
-
return result.ok ? decodeEvidencePage(result.value, (item) => provenanceCodec.decode(item).ok) : result;
|
|
864
|
+
return result.ok ? decodeEvidencePage(normalizeEvidencePageOutput(result.value, normalizeProvenanceOutput), (item) => provenanceCodec.decode(item).ok) : result;
|
|
828
865
|
}
|
|
829
866
|
};
|
|
830
867
|
const subscription = protocol.subscribe((event) => {
|
|
@@ -833,6 +870,7 @@ function createHostAdapter(options) {
|
|
|
833
870
|
});
|
|
834
871
|
const scopes = new WeakMap;
|
|
835
872
|
return {
|
|
873
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
836
874
|
pluginId: upstream.pluginId,
|
|
837
875
|
instanceId: protocol.instanceId,
|
|
838
876
|
async session(context) {
|
|
@@ -919,6 +957,119 @@ function createHostAdapter(options) {
|
|
|
919
957
|
}
|
|
920
958
|
};
|
|
921
959
|
}
|
|
960
|
+
function decodeExternalContribution(value, strict = true) {
|
|
961
|
+
if (!isRecord2(value) || value.version !== 1 || !isRecord2(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 && isRecord2(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)) {
|
|
962
|
+
return err("invalid-input", "Invalid V1 external contribution.");
|
|
963
|
+
}
|
|
964
|
+
let person = value.personAttribution;
|
|
965
|
+
if (!strict)
|
|
966
|
+
person = normalizeExternalPersonOutput(person);
|
|
967
|
+
if (strict && isRecord2(person) && isRecord2(person.presentation) && Object.keys(person.presentation).some((key) => !["displayName", "handle", "avatarUrl"].includes(key)))
|
|
968
|
+
return err("invalid-input", "Unexpected presentation field.");
|
|
969
|
+
const decoded = person === undefined ? null : externalPersonSnapshotCodec.decode(person);
|
|
970
|
+
if (decoded && !decoded.ok)
|
|
971
|
+
return decoded;
|
|
972
|
+
return ok({
|
|
973
|
+
version: 1,
|
|
974
|
+
author: { subject: value.author.subject, presentation: { displayName: value.author.presentation.displayName, handle: value.author.presentation.handle, avatarUrl: value.author.presentation.avatarUrl } },
|
|
975
|
+
...decoded?.ok ? { personAttribution: decoded.value } : {}
|
|
976
|
+
});
|
|
977
|
+
}
|
|
978
|
+
var externalContributionV1Codec = {
|
|
979
|
+
decode: (value) => decodeExternalContribution(value),
|
|
980
|
+
encode: (value) => value
|
|
981
|
+
};
|
|
982
|
+
function canonicalExternal(value) {
|
|
983
|
+
if (Array.isArray(value))
|
|
984
|
+
return `[${value.map(canonicalExternal).join(",")}]`;
|
|
985
|
+
if (isRecord2(value))
|
|
986
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalExternal(value[key])}`).join(",")}}`;
|
|
987
|
+
return JSON.stringify(value);
|
|
988
|
+
}
|
|
989
|
+
function decodeExternalContributionOutcomeV1(raw) {
|
|
990
|
+
const decoded = decodeAcceptance(raw);
|
|
991
|
+
if (!decoded.ok || decoded.value.status !== "submitted")
|
|
992
|
+
return decoded;
|
|
993
|
+
if (decoded.value.receipt.evidence !== "host-accepted")
|
|
994
|
+
return err("incompatible", "V1 needs durable native acceptance.");
|
|
995
|
+
const receipt = decoded.value.receipt;
|
|
996
|
+
const contribution = decodeExternalContribution(receipt.externalContribution, false);
|
|
997
|
+
return contribution.ok ? ok({ status: "submitted", receipt: { ...decoded.value.receipt, externalContribution: contribution.value } }) : err("incompatible", "Host receipt omitted or changed V1 attribution.");
|
|
998
|
+
}
|
|
999
|
+
function externalContributionMethods(upstream, extension, disposed) {
|
|
1000
|
+
const support = () => {
|
|
1001
|
+
if (disposed())
|
|
1002
|
+
return err("disposed", "Identity host is disposed.");
|
|
1003
|
+
if (extension.status === "absent")
|
|
1004
|
+
return ok(null);
|
|
1005
|
+
if (extension.status !== "supported")
|
|
1006
|
+
return { ok: false, error: protocolError(extension) };
|
|
1007
|
+
const value = extension.protocol.externalContributions;
|
|
1008
|
+
if (value === undefined)
|
|
1009
|
+
return ok(null);
|
|
1010
|
+
if (!isRecord2(value) || !Array.isArray(value.versions) || !value.versions.length || !value.versions.every((version) => Number.isInteger(version) && version > 0))
|
|
1011
|
+
return err("incompatible", "Malformed external contribution capability.");
|
|
1012
|
+
if (!value.versions.includes(1))
|
|
1013
|
+
return ok(null);
|
|
1014
|
+
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")
|
|
1015
|
+
return err("incompatible", "Malformed V1 external contribution capability.");
|
|
1016
|
+
return ok(value);
|
|
1017
|
+
};
|
|
1018
|
+
return {
|
|
1019
|
+
externalContributionCapabilities() {
|
|
1020
|
+
const negotiated = support();
|
|
1021
|
+
return !negotiated.ok ? negotiated : ok(negotiated.value ? { version: 1, personKinds: ["person"], history: "accepted-and-queued", rollback: "requires-capable-host" } : null);
|
|
1022
|
+
},
|
|
1023
|
+
async sendExternalContribution(contribution, input, options) {
|
|
1024
|
+
const operationId = input.operationId;
|
|
1025
|
+
const decoded = decodeExternalContribution(contribution);
|
|
1026
|
+
if (!decoded.ok)
|
|
1027
|
+
return { status: "rejected", error: decoded.error };
|
|
1028
|
+
const negotiated = support();
|
|
1029
|
+
if (!negotiated.ok)
|
|
1030
|
+
return { status: "rejected", error: negotiated.error };
|
|
1031
|
+
if (!negotiated.value)
|
|
1032
|
+
return { status: "rejected", error: { code: "unsupported", message: "Native external-person attribution V1 is unavailable.", retry: "never" } };
|
|
1033
|
+
if (options?.signal?.aborted)
|
|
1034
|
+
return { status: "rejected", error: { code: "expired", message: "The send was cancelled before dispatch.", retry: "never" } };
|
|
1035
|
+
const frozen = JSON.parse(JSON.stringify(decoded.value));
|
|
1036
|
+
const wire = JSON.parse(JSON.stringify(wireInput(input, upstream.inputCodec)));
|
|
1037
|
+
try {
|
|
1038
|
+
const result = decodeExternalContributionOutcomeV1(await negotiated.value.acceptV1({ contribution: frozen, input: wire }, options));
|
|
1039
|
+
if (!result.ok)
|
|
1040
|
+
return { status: "indeterminate", operationId, message: result.error.message };
|
|
1041
|
+
if (result.value.status === "submitted" && (result.value.receipt.operationId !== operationId || canonicalExternal(result.value.receipt.externalContribution) !== canonicalExternal(frozen))) {
|
|
1042
|
+
return { status: "indeterminate", operationId, message: "Host receipt did not confirm the reserved attribution." };
|
|
1043
|
+
}
|
|
1044
|
+
return result.value;
|
|
1045
|
+
} catch {
|
|
1046
|
+
return { status: "indeterminate", operationId, message: "External contribution dispatch outcome is unknown; reconcile this operation." };
|
|
1047
|
+
}
|
|
1048
|
+
},
|
|
1049
|
+
async lookupExternalContribution(operationId, options) {
|
|
1050
|
+
const negotiated = support();
|
|
1051
|
+
if (!negotiated.ok)
|
|
1052
|
+
return negotiated;
|
|
1053
|
+
if (!negotiated.value)
|
|
1054
|
+
return ok({ status: "unknown", reason: "unsupported" });
|
|
1055
|
+
try {
|
|
1056
|
+
const result = asResult(await negotiated.value.lookupV1(operationId, options));
|
|
1057
|
+
if (!result.ok)
|
|
1058
|
+
return result;
|
|
1059
|
+
if (isRecord2(result.value) && result.value.status === "final") {
|
|
1060
|
+
const decoded = decodeExternalContributionOutcomeV1(result.value.outcome);
|
|
1061
|
+
if (decoded.ok && decoded.value.status === "submitted" && decoded.value.receipt.operationId !== operationId) {
|
|
1062
|
+
return err("incompatible", "Host lookup returned a receipt for another operation.");
|
|
1063
|
+
}
|
|
1064
|
+
return decoded.ok ? ok({ status: "final", outcome: decoded.value }) : decoded;
|
|
1065
|
+
}
|
|
1066
|
+
return decodeLookup(result.value);
|
|
1067
|
+
} catch {
|
|
1068
|
+
return err("unavailable", "External contribution lookup is unavailable.", "after-reconnect");
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
}
|
|
922
1073
|
function inspectForkExtension(value) {
|
|
923
1074
|
if (value === undefined)
|
|
924
1075
|
return { status: "absent" };
|
|
@@ -1860,39 +2011,39 @@ function createBbUpstreamDriver(options) {
|
|
|
1860
2011
|
const active = new Set;
|
|
1861
2012
|
const listeners = new Set;
|
|
1862
2013
|
const labelledExternal = new WeakSet;
|
|
1863
|
-
const terminateScope = (
|
|
2014
|
+
const terminateScope = (record2, scope) => {
|
|
1864
2015
|
if (!scope || scope.released)
|
|
1865
2016
|
return;
|
|
1866
2017
|
scope.released = true;
|
|
1867
|
-
|
|
2018
|
+
record2.controller.signal.removeEventListener("abort", scope.parentAbort);
|
|
1868
2019
|
scope.controller.abort();
|
|
1869
|
-
|
|
2020
|
+
record2.scopes.delete(scope);
|
|
1870
2021
|
};
|
|
1871
|
-
const terminate = (
|
|
1872
|
-
for (const scope of [...
|
|
1873
|
-
terminateScope(
|
|
1874
|
-
|
|
1875
|
-
active.delete(
|
|
2022
|
+
const terminate = (record2) => {
|
|
2023
|
+
for (const scope of [...record2.scopes])
|
|
2024
|
+
terminateScope(record2, scope);
|
|
2025
|
+
record2.controller.abort();
|
|
2026
|
+
active.delete(record2);
|
|
1876
2027
|
};
|
|
1877
2028
|
const known = (context) => {
|
|
1878
|
-
const
|
|
1879
|
-
return !
|
|
2029
|
+
const record2 = records.get(context);
|
|
2030
|
+
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);
|
|
1880
2031
|
};
|
|
1881
2032
|
const admission = (context) => {
|
|
1882
|
-
const
|
|
1883
|
-
if (!
|
|
1884
|
-
return
|
|
1885
|
-
if (
|
|
2033
|
+
const record2 = known(context);
|
|
2034
|
+
if (!record2.ok)
|
|
2035
|
+
return record2;
|
|
2036
|
+
if (record2.value.origin === "external")
|
|
1886
2037
|
return err("unauthenticated", "External upstream work uses an explicit external source.");
|
|
1887
|
-
return
|
|
2038
|
+
return record2;
|
|
1888
2039
|
};
|
|
1889
2040
|
const session = (context) => {
|
|
1890
|
-
const
|
|
1891
|
-
if (!
|
|
1892
|
-
return unavailableSession(instanceId,
|
|
1893
|
-
if (
|
|
2041
|
+
const record2 = known(context);
|
|
2042
|
+
if (!record2.ok)
|
|
2043
|
+
return unavailableSession(instanceId, record2.error.code === "invalid-input" ? "incompatible" : "unauthenticated", record2.error.message);
|
|
2044
|
+
if (record2.value.origin === "interactive-user")
|
|
1894
2045
|
return readySession();
|
|
1895
|
-
return
|
|
2046
|
+
return record2.value.origin === "background" ? machineSession() : unavailableSession(instanceId, "unauthenticated", "External upstream work uses an explicit external source.");
|
|
1896
2047
|
};
|
|
1897
2048
|
const driver = {
|
|
1898
2049
|
instanceId,
|
|
@@ -1910,20 +2061,20 @@ function createBbUpstreamDriver(options) {
|
|
|
1910
2061
|
const allowed = admission(context);
|
|
1911
2062
|
if (!allowed.ok)
|
|
1912
2063
|
return allowed;
|
|
1913
|
-
const
|
|
2064
|
+
const record2 = allowed.value;
|
|
1914
2065
|
let scope;
|
|
1915
|
-
const parentAbort = () => terminateScope(
|
|
2066
|
+
const parentAbort = () => terminateScope(record2, scope);
|
|
1916
2067
|
scope = { controller: new AbortController, parentAbort, released: false };
|
|
1917
|
-
|
|
1918
|
-
const expire = () => terminateScope(
|
|
1919
|
-
|
|
1920
|
-
const exposedSession =
|
|
1921
|
-
const expectedKey =
|
|
2068
|
+
record2.scopes.add(scope);
|
|
2069
|
+
const expire = () => terminateScope(record2, scope);
|
|
2070
|
+
record2.controller.signal.addEventListener("abort", parentAbort, { once: true });
|
|
2071
|
+
const exposedSession = record2.origin === "background" ? machineSession() : readySession();
|
|
2072
|
+
const expectedKey = record2.origin === "background" ? machineKey.value : singletonKey;
|
|
1922
2073
|
return ok({
|
|
1923
2074
|
signal: scope.controller.signal,
|
|
1924
2075
|
session: exposedSession,
|
|
1925
2076
|
validate(expected) {
|
|
1926
|
-
if (scope.released || scope.controller.signal.aborted ||
|
|
2077
|
+
if (scope.released || scope.controller.signal.aborted || record2.controller.signal.aborted || disposed) {
|
|
1927
2078
|
return err("expired", "Upstream request context is no longer live.");
|
|
1928
2079
|
}
|
|
1929
2080
|
return expected.actor === expectedKey && expected.session === stamp.value ? ok(undefined) : err("stale-context", "Request expectation does not match the singleton session.");
|
|
@@ -2007,22 +2158,22 @@ function createBbUpstreamDriver(options) {
|
|
|
2007
2158
|
throw new Error("Upstream binding is disposed.");
|
|
2008
2159
|
const controller = new AbortController;
|
|
2009
2160
|
const invocation = Object.freeze({ origin, signal: controller.signal });
|
|
2010
|
-
const
|
|
2011
|
-
records.set(invocation,
|
|
2012
|
-
active.add(
|
|
2161
|
+
const record2 = { origin, controller, scopes: new Set };
|
|
2162
|
+
records.set(invocation, record2);
|
|
2163
|
+
active.add(record2);
|
|
2013
2164
|
return invocation;
|
|
2014
2165
|
},
|
|
2015
2166
|
release(invocation) {
|
|
2016
|
-
const
|
|
2017
|
-
if (
|
|
2018
|
-
terminate(
|
|
2167
|
+
const record2 = records.get(invocation);
|
|
2168
|
+
if (record2)
|
|
2169
|
+
terminate(record2);
|
|
2019
2170
|
},
|
|
2020
2171
|
dispose() {
|
|
2021
2172
|
if (disposed)
|
|
2022
2173
|
return;
|
|
2023
2174
|
disposed = true;
|
|
2024
|
-
for (const
|
|
2025
|
-
terminate(
|
|
2175
|
+
for (const record2 of [...active])
|
|
2176
|
+
terminate(record2);
|
|
2026
2177
|
for (const listener of [...listeners]) {
|
|
2027
2178
|
try {
|
|
2028
2179
|
listener({ kind: "disposed" });
|
|
@@ -2187,6 +2338,9 @@ function createIdentityServer(options) {
|
|
|
2187
2338
|
},
|
|
2188
2339
|
toolProvenance: (context) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.readToolProvenance(context),
|
|
2189
2340
|
sendExternal: (author, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternal(author, input, readOptions),
|
|
2341
|
+
externalContributionCapabilities: () => disposed ? err("disposed", "Identity server is disposed.") : host.externalContributionCapabilities(),
|
|
2342
|
+
sendExternalContribution: (contribution, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternalContribution(contribution, input, readOptions),
|
|
2343
|
+
lookupExternalContribution: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupExternalContribution(operationId, readOptions),
|
|
2190
2344
|
lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupOperation(operationId, readOptions),
|
|
2191
2345
|
dispose
|
|
2192
2346
|
};
|
|
@@ -2646,8 +2800,8 @@ function createBbIdentityServerBinding(options) {
|
|
|
2646
2800
|
const invocations = new WeakMap;
|
|
2647
2801
|
const expired = () => err("expired", "Invocation has already settled.");
|
|
2648
2802
|
const recordFor = (invocation) => {
|
|
2649
|
-
const
|
|
2650
|
-
return disposed || !
|
|
2803
|
+
const record2 = invocations.get(invocation);
|
|
2804
|
+
return disposed || !record2 || record2.settled || record2.signal.aborted ? expired() : ok(record2);
|
|
2651
2805
|
};
|
|
2652
2806
|
const unavailableSession2 = () => ({
|
|
2653
2807
|
status: "unavailable",
|
|
@@ -2655,25 +2809,25 @@ function createBbIdentityServerBinding(options) {
|
|
|
2655
2809
|
error: { code: "expired", message: "Invocation has already settled.", retry: "after-refresh" }
|
|
2656
2810
|
});
|
|
2657
2811
|
const openPerson = async (invocation) => {
|
|
2658
|
-
const
|
|
2659
|
-
if (!
|
|
2660
|
-
return
|
|
2661
|
-
const opened = await server.personRequest(
|
|
2662
|
-
if (
|
|
2812
|
+
const record2 = recordFor(invocation);
|
|
2813
|
+
if (!record2.ok)
|
|
2814
|
+
return record2;
|
|
2815
|
+
const opened = await server.personRequest(record2.value.request);
|
|
2816
|
+
if (record2.value.settled || record2.value.signal.aborted) {
|
|
2663
2817
|
if (opened.ok)
|
|
2664
2818
|
opened.value.dispose();
|
|
2665
2819
|
return expired();
|
|
2666
2820
|
}
|
|
2667
2821
|
if (opened.ok)
|
|
2668
|
-
|
|
2822
|
+
record2.value.people.add(opened.value);
|
|
2669
2823
|
return opened;
|
|
2670
2824
|
};
|
|
2671
2825
|
const guardEndpoint = async (invocation, operation) => {
|
|
2672
|
-
const
|
|
2673
|
-
if (!
|
|
2674
|
-
return
|
|
2675
|
-
const result = await operation(
|
|
2676
|
-
return
|
|
2826
|
+
const record2 = recordFor(invocation);
|
|
2827
|
+
if (!record2.ok)
|
|
2828
|
+
return record2;
|
|
2829
|
+
const result = await operation(record2.value.request);
|
|
2830
|
+
return record2.value.settled || record2.value.signal.aborted ? expired() : result;
|
|
2677
2831
|
};
|
|
2678
2832
|
let disposeBinding = () => {};
|
|
2679
2833
|
const guardedHistory = {
|
|
@@ -2694,11 +2848,11 @@ function createBbIdentityServerBinding(options) {
|
|
|
2694
2848
|
instanceId: server.instanceId,
|
|
2695
2849
|
commits: server.commits,
|
|
2696
2850
|
async session(invocation) {
|
|
2697
|
-
const
|
|
2698
|
-
if (!
|
|
2851
|
+
const record2 = recordFor(invocation);
|
|
2852
|
+
if (!record2.ok)
|
|
2699
2853
|
return unavailableSession2();
|
|
2700
|
-
const session = await server.session(
|
|
2701
|
-
return
|
|
2854
|
+
const session = await server.session(record2.value.request);
|
|
2855
|
+
return record2.value.settled || record2.value.signal.aborted ? unavailableSession2() : session;
|
|
2702
2856
|
},
|
|
2703
2857
|
selfProfile: (invocation) => guardEndpoint(invocation, (request) => server.selfProfile(request)),
|
|
2704
2858
|
personRequest: openPerson,
|
|
@@ -2708,6 +2862,17 @@ function createBbIdentityServerBinding(options) {
|
|
|
2708
2862
|
const renderedInput = upstream.value.externalMessageRendering === "host" ? renderExternalPrompt(upstream.value.driver.pluginId, author, input.input) : input.input;
|
|
2709
2863
|
return server.sendExternal(author, { ...input, input: renderedInput }, readOptions);
|
|
2710
2864
|
},
|
|
2865
|
+
externalContributionCapabilities: () => disposed ? err("disposed", "Identity binding is disposed.") : server.externalContributionCapabilities(),
|
|
2866
|
+
async sendExternalContribution(contribution, input, readOptions) {
|
|
2867
|
+
if (disposed)
|
|
2868
|
+
return { status: "rejected", error: { code: "disposed", message: "Identity binding is disposed.", retry: "never" } };
|
|
2869
|
+
const decoded = externalContributionV1Codec.decode(contribution);
|
|
2870
|
+
if (!decoded.ok)
|
|
2871
|
+
return { status: "rejected", error: decoded.error };
|
|
2872
|
+
const renderedInput = upstream.value.externalMessageRendering === "host" ? renderExternalPrompt(upstream.value.driver.pluginId, decoded.value.author, input.input) : input.input;
|
|
2873
|
+
return server.sendExternalContribution(decoded.value, { ...input, input: renderedInput }, readOptions);
|
|
2874
|
+
},
|
|
2875
|
+
lookupExternalContribution: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.lookupExternalContribution(operationId, readOptions),
|
|
2711
2876
|
lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.lookupOperation(operationId, readOptions),
|
|
2712
2877
|
history: guardedHistory
|
|
2713
2878
|
};
|
|
@@ -2764,18 +2929,18 @@ function createBbIdentityServerBinding(options) {
|
|
|
2764
2929
|
return endpointRegistered;
|
|
2765
2930
|
}
|
|
2766
2931
|
const createInvocation = (context) => {
|
|
2767
|
-
const
|
|
2932
|
+
const record2 = { request: context.request, signal: context.signal, people: new Set, settled: false };
|
|
2768
2933
|
const invocation = {
|
|
2769
2934
|
origin: context.origin,
|
|
2770
2935
|
signal: context.signal,
|
|
2771
2936
|
person: () => context.origin === "external" ? Promise.resolve(err("unauthenticated", "External invocation uses an explicit external source.")) : openPerson(invocation)
|
|
2772
2937
|
};
|
|
2773
|
-
invocations.set(invocation,
|
|
2938
|
+
invocations.set(invocation, record2);
|
|
2774
2939
|
const finish = () => {
|
|
2775
|
-
|
|
2776
|
-
for (const request of
|
|
2940
|
+
record2.settled = true;
|
|
2941
|
+
for (const request of record2.people)
|
|
2777
2942
|
request.dispose();
|
|
2778
|
-
|
|
2943
|
+
record2.people.clear();
|
|
2779
2944
|
};
|
|
2780
2945
|
return { invocation, finish };
|
|
2781
2946
|
};
|
|
@@ -2991,7 +3156,7 @@ function createMultiUserProtocol(instanceId, initial) {
|
|
|
2991
3156
|
}
|
|
2992
3157
|
};
|
|
2993
3158
|
};
|
|
2994
|
-
const
|
|
3159
|
+
const record2 = (subject) => {
|
|
2995
3160
|
const person = people.get(subject);
|
|
2996
3161
|
return person ? { issuer: ISSUER, subject, presentation: { displayName: person.name, handle: null, avatarUrl: null }, status: "current" } : null;
|
|
2997
3162
|
};
|
|
@@ -3001,11 +3166,11 @@ function createMultiUserProtocol(instanceId, initial) {
|
|
|
3001
3166
|
generation: brand("provider-generation", "wire-fake-provider"),
|
|
3002
3167
|
async directory(input) {
|
|
3003
3168
|
const query = input.query.toLowerCase();
|
|
3004
|
-
const records = [...people.keys()].map(
|
|
3169
|
+
const records = [...people.keys()].map(record2).filter((r) => r !== null && (r.subject.includes(query) || r.presentation.displayName.toLowerCase().includes(query))).slice(0, input.limit);
|
|
3005
3170
|
return ok({ records, nextCursor: null, revision: revision() });
|
|
3006
3171
|
},
|
|
3007
3172
|
async lookup(subjects) {
|
|
3008
|
-
return ok({ revision: revision(), records: subjects.map((s) => ({ issuer: s.issuer, subject: s.subject, record: s.issuer === ISSUER ?
|
|
3173
|
+
return ok({ revision: revision(), records: subjects.map((s) => ({ issuer: s.issuer, subject: s.subject, record: s.issuer === ISSUER ? record2(s.subject) : null })) });
|
|
3009
3174
|
},
|
|
3010
3175
|
person(issuer, subject) {
|
|
3011
3176
|
const key = identityKeyCodec.decode(`${issuer}:${subject}`);
|
|
@@ -3257,8 +3422,8 @@ function canonical(value) {
|
|
|
3257
3422
|
return JSON.stringify(value) ?? "undefined";
|
|
3258
3423
|
if (Array.isArray(value))
|
|
3259
3424
|
return "[" + value.map(canonical).join(",") + "]";
|
|
3260
|
-
const
|
|
3261
|
-
return "{" + Object.keys(
|
|
3425
|
+
const record2 = value;
|
|
3426
|
+
return "{" + Object.keys(record2).filter((k) => record2[k] !== undefined).sort().map((k) => JSON.stringify(k) + ":" + canonical(record2[k])).join(",") + "}";
|
|
3262
3427
|
}
|
|
3263
3428
|
function id(kind, value) {
|
|
3264
3429
|
const decoded = idCodec(kind).decode(value);
|
package/host.d.ts
CHANGED
|
@@ -37,6 +37,30 @@ export type HostInvalidation =
|
|
|
37
37
|
| { readonly kind: 'participants'; readonly threadIds: readonly string[] }
|
|
38
38
|
| { readonly kind: 'disconnected' | 'reconnected' | 'disposed' };
|
|
39
39
|
export interface ExternalAuthorInput { readonly subject: string; readonly presentation: ProfilePresentation; }
|
|
40
|
+
export interface ExternalContributionV1 {
|
|
41
|
+
readonly version: 1;
|
|
42
|
+
readonly author: ExternalAuthorInput;
|
|
43
|
+
readonly personAttribution?: import('./model.js').ExternalPersonSnapshotV1;
|
|
44
|
+
}
|
|
45
|
+
export declare const externalContributionV1Codec: Codec<ExternalContributionV1>;
|
|
46
|
+
export type ExternalContributionOutcomeV1 = Exclude<AcceptanceOutcome, { status: 'submitted' }> | {
|
|
47
|
+
readonly status: 'submitted';
|
|
48
|
+
readonly receipt: Extract<import('./model.js').AcceptanceReceipt, { evidence: 'host-accepted' }> & {
|
|
49
|
+
readonly externalContribution: ExternalContributionV1;
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
export declare const externalContributionOutcomeV1Codec: Codec<ExternalContributionOutcomeV1>;
|
|
53
|
+
/** Context-free support; native storage written by V1 requires a capable host on rollback. */
|
|
54
|
+
export interface ExternalContributionCapabilityV1 {
|
|
55
|
+
readonly version: 1; readonly personKinds: readonly ['person'];
|
|
56
|
+
readonly history: 'accepted-and-queued'; readonly rollback: 'requires-capable-host';
|
|
57
|
+
}
|
|
58
|
+
export interface ExternalContributionProtocolV1<Input> {
|
|
59
|
+
readonly versions: readonly number[]; readonly personKinds: readonly ['person'];
|
|
60
|
+
readonly history: 'accepted-and-queued'; readonly rollback: 'requires-capable-host';
|
|
61
|
+
acceptV1(request: { readonly contribution: Wire<ExternalContributionV1>; readonly input: Wire<SendInput<Input>> }, options?: ReadOptions): Promise<Wire<ExternalContributionOutcomeV1>>;
|
|
62
|
+
lookupV1(operationId: string, options?: ReadOptions): Promise<Wire<Result<OperationLookup<ExternalContributionOutcomeV1>>>>;
|
|
63
|
+
}
|
|
40
64
|
export interface IdentityHost<RequestContext, ToolContext, Input> extends Disposable {
|
|
41
65
|
readonly pluginId: PluginId;
|
|
42
66
|
readonly instanceId: InstanceId;
|
|
@@ -51,6 +75,10 @@ export interface IdentityHost<RequestContext, ToolContext, Input> extends Dispos
|
|
|
51
75
|
/** Checks synchronously before upstream dispatch; enhanced host checks again inside acceptance. */
|
|
52
76
|
send(request: LiveRequest, input: SendInput<Input>, options?: ReadOptions): Promise<AcceptanceOutcome>;
|
|
53
77
|
sendExternal(author: ExternalAuthorInput, input: SendInput<Input>, options?: ReadOptions): Promise<AcceptanceOutcome>;
|
|
78
|
+
externalContributionCapabilities(): Result<ExternalContributionCapabilityV1 | null>;
|
|
79
|
+
sendExternalContribution(contribution: ExternalContributionV1, input: SendInput<Input>, options?: ReadOptions): Promise<ExternalContributionOutcomeV1>;
|
|
80
|
+
lookupExternalContribution(operationId: SendInput<Input>['operationId'], options?: ReadOptions): Promise<Result<OperationLookup<ExternalContributionOutcomeV1>>>;
|
|
81
|
+
|
|
54
82
|
lookupOperation(operationId: SendInput<Input>['operationId'], options?: ReadOptions): Promise<Result<OperationLookup<AcceptanceOutcome>>>;
|
|
55
83
|
forward(request: LiveRequest, destinationPlugin: string, method: string, input: import('./model.js').Json): Promise<unknown>;
|
|
56
84
|
registerProvider(provider: IdentityProvider): Promise<Result<ProviderRegistrationV1>>;
|
|
@@ -149,6 +177,7 @@ export interface ForkIdentityProvider {
|
|
|
149
177
|
/** Core implements these host-owned operations, not IdentityHost's fallback/directory composition. */
|
|
150
178
|
export interface ForkIdentityProtocolV1<RequestContext, ToolContext, Input> {
|
|
151
179
|
readonly version: 1;
|
|
180
|
+
readonly externalContributions?: ExternalContributionProtocolV1<Input>;
|
|
152
181
|
/** Persisted core storage namespace; stable across plugin replacement and never request-derived. */
|
|
153
182
|
readonly instanceId: string;
|
|
154
183
|
/** Enables this plugin generation's producer-owned external message rendering in the SDK host. */
|
package/model.d.ts
CHANGED
|
@@ -99,10 +99,17 @@ export interface ContributionReference {
|
|
|
99
99
|
readonly threadId: ThreadId;
|
|
100
100
|
readonly contributionId: ContributionId;
|
|
101
101
|
}
|
|
102
|
+
/** Frozen integration assertion; V1 supports canonical people, not default-user. */
|
|
103
|
+
export type ExternalPersonSnapshotV1 = ActorSnapshot & {
|
|
104
|
+
readonly identity: Extract<IdentityReference, { kind: 'person' }>;
|
|
105
|
+
readonly evidence: 'integration-asserted';
|
|
106
|
+
};
|
|
107
|
+
export declare const externalPersonSnapshotCodec: Codec<ExternalPersonSnapshotV1>;
|
|
102
108
|
/** A contribution is independent of delivery grouping and execution attempts. */
|
|
103
109
|
export interface Contribution {
|
|
104
110
|
readonly reference: ContributionReference;
|
|
105
111
|
readonly author: Origin;
|
|
112
|
+
readonly personAttribution?: ExternalPersonSnapshotV1;
|
|
106
113
|
readonly latestEditor: ActorSnapshot | null;
|
|
107
114
|
readonly acceptedAt: string;
|
|
108
115
|
readonly mentionedPeople: readonly PersonReference[];
|
package/package.json
CHANGED
package/server.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import type { AcceptanceOutcome, ActorReference, ActorSnapshot, Codec, Directory, Disposable, ExecutionProvenance,
|
|
3
3
|
IdentityKey, InstanceId, OperationId, OperationLookup, ParticipantReader, PersonReference,
|
|
4
4
|
PersonSendInput, ReadOptions, RequestExpectation, Result, SendInput, TargetSelection, Unsubscribe } from './model.js';
|
|
5
|
-
import type { ExternalAuthorInput, IdentityHost, IdentityCapabilities, ServerSession, HostInvalidation } from './host.js';
|
|
5
|
+
import type { ExternalAuthorInput, ExternalContributionV1, ExternalContributionOutcomeV1, ExternalContributionCapabilityV1, IdentityHost, IdentityCapabilities, ServerSession, HostInvalidation } from './host.js';
|
|
6
6
|
|
|
7
7
|
declare const resolvedTarget: unique symbol;
|
|
8
8
|
export interface TargetSnapshot {
|
|
@@ -61,6 +61,10 @@ export interface IdentityServer<RequestContext, ToolContext, Input> extends Disp
|
|
|
61
61
|
personRequest(context: RequestContext, options?: ReadOptions): Promise<Result<PersonRequest<Input>>>;
|
|
62
62
|
toolProvenance(context: ToolContext): Promise<Result<ExecutionProvenance>>;
|
|
63
63
|
sendExternal(author: ExternalAuthorInput, input: SendInput<Input>, options?: ReadOptions): Promise<AcceptanceOutcome>;
|
|
64
|
+
externalContributionCapabilities(): Result<ExternalContributionCapabilityV1 | null>;
|
|
65
|
+
sendExternalContribution(contribution: ExternalContributionV1, input: SendInput<Input>, options?: ReadOptions): Promise<ExternalContributionOutcomeV1>;
|
|
66
|
+
lookupExternalContribution(operationId: SendInput<Input>['operationId'], options?: ReadOptions): Promise<Result<OperationLookup<ExternalContributionOutcomeV1>>>;
|
|
67
|
+
|
|
64
68
|
/** Lookup reports retained operation outcomes; IDs are scoped to this instance/plugin. */
|
|
65
69
|
lookupOperation(operationId: OperationId, options?: ReadOptions): Promise<Result<OperationLookup<AcceptanceOutcome>>>;
|
|
66
70
|
}
|