@phosphorco/bb-identity 0.2.0 → 0.3.0
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 +14 -0
- package/bb.d.ts +3 -0
- package/dist/bb-entry-runtime.js +147 -2
- package/dist/host-runtime.js +134 -2
- package/dist/index-runtime.js +12 -1
- package/dist/model-runtime.js +13 -1
- package/dist/server-runtime.js +3 -0
- package/dist/testing-runtime.js +147 -2
- package/host.d.ts +28 -0
- package/model.d.ts +7 -0
- package/package.json +3 -3
- package/server.d.ts +5 -1
package/README.md
CHANGED
|
@@ -463,3 +463,17 @@ 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.
|
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'];
|
package/dist/bb-entry-runtime.js
CHANGED
|
@@ -172,7 +172,7 @@ function isOrigin(value) {
|
|
|
172
172
|
return value.kind === "unknown" && (value.reason === "legacy" || value.reason === "upstream-unattributed" || value.reason === "missing-source");
|
|
173
173
|
}
|
|
174
174
|
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);
|
|
175
|
+
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
176
|
}
|
|
177
177
|
function isCorrelation(value) {
|
|
178
178
|
return isRecord(value) && idCodec("thread").decode(value.threadId).ok && isBoundedString(value.turnId) && isBoundedString(value.attemptId) && (value.toolCallId === null || isBoundedString(value.toolCallId));
|
|
@@ -249,6 +249,17 @@ var provenanceCodec = {
|
|
|
249
249
|
return value;
|
|
250
250
|
}
|
|
251
251
|
};
|
|
252
|
+
var externalPersonSnapshotCodec = {
|
|
253
|
+
decode(value) {
|
|
254
|
+
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))) {
|
|
255
|
+
return err("invalid-input", "External person attribution requires a canonical person and integration-asserted evidence.");
|
|
256
|
+
}
|
|
257
|
+
return ok(value);
|
|
258
|
+
},
|
|
259
|
+
encode(value) {
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
};
|
|
252
263
|
function defaultIdentityKey(instance) {
|
|
253
264
|
return `local:${instance}`;
|
|
254
265
|
}
|
|
@@ -766,7 +777,7 @@ function isContribution2(value) {
|
|
|
766
777
|
if (!isRecord3(origin))
|
|
767
778
|
return false;
|
|
768
779
|
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));
|
|
780
|
+
return (value.personAttribution === undefined || externalPersonSnapshotCodec.decode(value.personAttribution).ok) && validOrigin && (value.latestEditor === null || snapshot(value.latestEditor));
|
|
770
781
|
}
|
|
771
782
|
function decodeEvidencePage(value, decodeItem) {
|
|
772
783
|
if (!isRecord3(value) || typeof value.status !== "string")
|
|
@@ -1101,6 +1112,7 @@ function createHostAdapter(options) {
|
|
|
1101
1112
|
});
|
|
1102
1113
|
const contexts = new WeakMap;
|
|
1103
1114
|
return {
|
|
1115
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
1104
1116
|
pluginId: upstream.pluginId,
|
|
1105
1117
|
instanceId: upstream.instanceId,
|
|
1106
1118
|
session: (context) => upstream.session(context),
|
|
@@ -1146,6 +1158,7 @@ function createHostAdapter(options) {
|
|
|
1146
1158
|
const error = protocolError(extension);
|
|
1147
1159
|
const unavailableResult = async () => ({ ok: false, error });
|
|
1148
1160
|
return {
|
|
1161
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
1149
1162
|
pluginId: upstream.pluginId,
|
|
1150
1163
|
instanceId: upstream.instanceId,
|
|
1151
1164
|
session: async () => ({ status: error.code === "incompatible" ? "incompatible" : "unavailable", instanceId: upstream.instanceId, error }),
|
|
@@ -1221,6 +1234,7 @@ function createHostAdapter(options) {
|
|
|
1221
1234
|
});
|
|
1222
1235
|
const scopes = new WeakMap;
|
|
1223
1236
|
return {
|
|
1237
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
1224
1238
|
pluginId: upstream.pluginId,
|
|
1225
1239
|
instanceId: protocol.instanceId,
|
|
1226
1240
|
async session(context) {
|
|
@@ -1307,6 +1321,123 @@ function createHostAdapter(options) {
|
|
|
1307
1321
|
}
|
|
1308
1322
|
};
|
|
1309
1323
|
}
|
|
1324
|
+
function decodeExternalContribution(value, strict = true) {
|
|
1325
|
+
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)) || !idCodec("subject").decode(value.author.subject).ok || !isProfilePresentation(value.author.presentation)) {
|
|
1326
|
+
return err("invalid-input", "Invalid V1 external contribution.");
|
|
1327
|
+
}
|
|
1328
|
+
let person = value.personAttribution;
|
|
1329
|
+
if (!strict && isRecord3(person) && isRecord3(person.identity))
|
|
1330
|
+
person = {
|
|
1331
|
+
identity: { kind: person.identity.kind, key: person.identity.key, issuer: person.identity.issuer, subject: person.identity.subject },
|
|
1332
|
+
presentation: isRecord3(person.presentation) ? { displayName: person.presentation.displayName, handle: person.presentation.handle, avatarUrl: person.presentation.avatarUrl } : person.presentation,
|
|
1333
|
+
evidence: person.evidence
|
|
1334
|
+
};
|
|
1335
|
+
if (strict && isRecord3(person) && isRecord3(person.presentation) && Object.keys(person.presentation).some((key) => !["displayName", "handle", "avatarUrl"].includes(key)))
|
|
1336
|
+
return err("invalid-input", "Unexpected presentation field.");
|
|
1337
|
+
const decoded = person === undefined ? null : externalPersonSnapshotCodec.decode(person);
|
|
1338
|
+
if (decoded && !decoded.ok)
|
|
1339
|
+
return decoded;
|
|
1340
|
+
return ok({
|
|
1341
|
+
version: 1,
|
|
1342
|
+
author: { subject: value.author.subject, presentation: { displayName: value.author.presentation.displayName, handle: value.author.presentation.handle, avatarUrl: value.author.presentation.avatarUrl } },
|
|
1343
|
+
...decoded?.ok ? { personAttribution: decoded.value } : {}
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
var externalContributionV1Codec = {
|
|
1347
|
+
decode: (value) => decodeExternalContribution(value),
|
|
1348
|
+
encode: (value) => value
|
|
1349
|
+
};
|
|
1350
|
+
function canonicalExternal(value) {
|
|
1351
|
+
if (Array.isArray(value))
|
|
1352
|
+
return `[${value.map(canonicalExternal).join(",")}]`;
|
|
1353
|
+
if (isRecord3(value))
|
|
1354
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalExternal(value[key])}`).join(",")}}`;
|
|
1355
|
+
return JSON.stringify(value);
|
|
1356
|
+
}
|
|
1357
|
+
function externalContributionMethods(upstream, extension, disposed) {
|
|
1358
|
+
const support = () => {
|
|
1359
|
+
if (disposed())
|
|
1360
|
+
return err("disposed", "Identity host is disposed.");
|
|
1361
|
+
if (extension.status === "absent")
|
|
1362
|
+
return ok(null);
|
|
1363
|
+
if (extension.status !== "supported")
|
|
1364
|
+
return { ok: false, error: protocolError(extension) };
|
|
1365
|
+
const value = extension.protocol.externalContributions;
|
|
1366
|
+
if (value === undefined)
|
|
1367
|
+
return ok(null);
|
|
1368
|
+
if (!isRecord3(value) || !Array.isArray(value.versions) || !value.versions.length || !value.versions.every((version) => Number.isInteger(version) && version > 0))
|
|
1369
|
+
return err("incompatible", "Malformed external contribution capability.");
|
|
1370
|
+
if (!value.versions.includes(1))
|
|
1371
|
+
return ok(null);
|
|
1372
|
+
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")
|
|
1373
|
+
return err("incompatible", "Malformed V1 external contribution capability.");
|
|
1374
|
+
return ok(value);
|
|
1375
|
+
};
|
|
1376
|
+
const outcome = (raw) => {
|
|
1377
|
+
const decoded = decodeAcceptance(raw);
|
|
1378
|
+
if (!decoded.ok || decoded.value.status !== "submitted")
|
|
1379
|
+
return decoded;
|
|
1380
|
+
if (decoded.value.receipt.evidence !== "host-accepted")
|
|
1381
|
+
return err("incompatible", "V1 needs durable native acceptance.");
|
|
1382
|
+
const receipt = decoded.value.receipt;
|
|
1383
|
+
const contribution = decodeExternalContribution(receipt.externalContribution, false);
|
|
1384
|
+
return contribution.ok ? ok({ status: "submitted", receipt: { ...decoded.value.receipt, externalContribution: contribution.value } }) : err("incompatible", "Host receipt omitted or changed V1 attribution.");
|
|
1385
|
+
};
|
|
1386
|
+
return {
|
|
1387
|
+
externalContributionCapabilities() {
|
|
1388
|
+
const negotiated = support();
|
|
1389
|
+
return !negotiated.ok ? negotiated : ok(negotiated.value ? { version: 1, personKinds: ["person"], history: "accepted-and-queued", rollback: "requires-capable-host" } : null);
|
|
1390
|
+
},
|
|
1391
|
+
async sendExternalContribution(contribution, input, options) {
|
|
1392
|
+
const operationId = input.operationId;
|
|
1393
|
+
const decoded = decodeExternalContribution(contribution);
|
|
1394
|
+
if (!decoded.ok)
|
|
1395
|
+
return { status: "rejected", error: decoded.error };
|
|
1396
|
+
const negotiated = support();
|
|
1397
|
+
if (!negotiated.ok)
|
|
1398
|
+
return { status: "rejected", error: negotiated.error };
|
|
1399
|
+
if (!negotiated.value)
|
|
1400
|
+
return { status: "rejected", error: { code: "unsupported", message: "Native external-person attribution V1 is unavailable.", retry: "never" } };
|
|
1401
|
+
if (options?.signal?.aborted)
|
|
1402
|
+
return { status: "rejected", error: { code: "expired", message: "The send was cancelled before dispatch.", retry: "never" } };
|
|
1403
|
+
const frozen = JSON.parse(JSON.stringify(decoded.value));
|
|
1404
|
+
const wire = JSON.parse(JSON.stringify(wireInput(input, upstream.inputCodec)));
|
|
1405
|
+
try {
|
|
1406
|
+
const result = outcome(await negotiated.value.acceptV1({ contribution: frozen, input: wire }, options));
|
|
1407
|
+
if (!result.ok)
|
|
1408
|
+
return { status: "indeterminate", operationId, message: result.error.message };
|
|
1409
|
+
if (result.value.status === "submitted" && (result.value.receipt.operationId !== operationId || canonicalExternal(result.value.receipt.externalContribution) !== canonicalExternal(frozen))) {
|
|
1410
|
+
return { status: "indeterminate", operationId, message: "Host receipt did not confirm the reserved attribution." };
|
|
1411
|
+
}
|
|
1412
|
+
return result.value;
|
|
1413
|
+
} catch {
|
|
1414
|
+
return { status: "indeterminate", operationId, message: "External contribution dispatch outcome is unknown; reconcile this operation." };
|
|
1415
|
+
}
|
|
1416
|
+
},
|
|
1417
|
+
async lookupExternalContribution(operationId, options) {
|
|
1418
|
+
const negotiated = support();
|
|
1419
|
+
if (!negotiated.ok)
|
|
1420
|
+
return negotiated;
|
|
1421
|
+
if (!negotiated.value)
|
|
1422
|
+
return ok({ status: "unknown", reason: "unsupported" });
|
|
1423
|
+
try {
|
|
1424
|
+
const result = asResult(await negotiated.value.lookupV1(operationId, options));
|
|
1425
|
+
if (!result.ok)
|
|
1426
|
+
return result;
|
|
1427
|
+
if (isRecord3(result.value) && result.value.status === "final") {
|
|
1428
|
+
const decoded = outcome(result.value.outcome);
|
|
1429
|
+
if (decoded.ok && decoded.value.status === "submitted" && decoded.value.receipt.operationId !== operationId) {
|
|
1430
|
+
return err("incompatible", "Host lookup returned a receipt for another operation.");
|
|
1431
|
+
}
|
|
1432
|
+
return decoded.ok ? ok({ status: "final", outcome: decoded.value }) : decoded;
|
|
1433
|
+
}
|
|
1434
|
+
return decodeLookup(result.value);
|
|
1435
|
+
} catch {
|
|
1436
|
+
return err("unavailable", "External contribution lookup is unavailable.", "after-reconnect");
|
|
1437
|
+
}
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1310
1441
|
function inspectForkExtension(value) {
|
|
1311
1442
|
if (value === undefined)
|
|
1312
1443
|
return { status: "absent" };
|
|
@@ -1464,6 +1595,9 @@ function createIdentityServer(options) {
|
|
|
1464
1595
|
},
|
|
1465
1596
|
toolProvenance: (context) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.readToolProvenance(context),
|
|
1466
1597
|
sendExternal: (author, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternal(author, input, readOptions),
|
|
1598
|
+
externalContributionCapabilities: () => disposed ? err("disposed", "Identity server is disposed.") : host.externalContributionCapabilities(),
|
|
1599
|
+
sendExternalContribution: (contribution, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternalContribution(contribution, input, readOptions),
|
|
1600
|
+
lookupExternalContribution: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupExternalContribution(operationId, readOptions),
|
|
1467
1601
|
lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupOperation(operationId, readOptions),
|
|
1468
1602
|
dispose
|
|
1469
1603
|
};
|
|
@@ -2345,6 +2479,17 @@ function createBbIdentityServerBinding(options) {
|
|
|
2345
2479
|
const renderedInput = upstream.value.externalMessageRendering === "host" ? renderExternalPrompt(upstream.value.driver.pluginId, author, input.input) : input.input;
|
|
2346
2480
|
return server.sendExternal(author, { ...input, input: renderedInput }, readOptions);
|
|
2347
2481
|
},
|
|
2482
|
+
externalContributionCapabilities: () => disposed ? err("disposed", "Identity binding is disposed.") : server.externalContributionCapabilities(),
|
|
2483
|
+
async sendExternalContribution(contribution, input, readOptions) {
|
|
2484
|
+
if (disposed)
|
|
2485
|
+
return { status: "rejected", error: { code: "disposed", message: "Identity binding is disposed.", retry: "never" } };
|
|
2486
|
+
const decoded = externalContributionV1Codec.decode(contribution);
|
|
2487
|
+
if (!decoded.ok)
|
|
2488
|
+
return { status: "rejected", error: decoded.error };
|
|
2489
|
+
const renderedInput = upstream.value.externalMessageRendering === "host" ? renderExternalPrompt(upstream.value.driver.pluginId, decoded.value.author, input.input) : input.input;
|
|
2490
|
+
return server.sendExternalContribution(decoded.value, { ...input, input: renderedInput }, readOptions);
|
|
2491
|
+
},
|
|
2492
|
+
lookupExternalContribution: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.lookupExternalContribution(operationId, readOptions),
|
|
2348
2493
|
lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.lookupOperation(operationId, readOptions),
|
|
2349
2494
|
history: guardedHistory
|
|
2350
2495
|
};
|
package/dist/host-runtime.js
CHANGED
|
@@ -107,7 +107,7 @@ function isOrigin(value) {
|
|
|
107
107
|
return value.kind === "unknown" && (value.reason === "legacy" || value.reason === "upstream-unattributed" || value.reason === "missing-source");
|
|
108
108
|
}
|
|
109
109
|
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);
|
|
110
|
+
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
111
|
}
|
|
112
112
|
function isCorrelation(value) {
|
|
113
113
|
return isRecord(value) && idCodec("thread").decode(value.threadId).ok && isBoundedString(value.turnId) && isBoundedString(value.attemptId) && (value.toolCallId === null || isBoundedString(value.toolCallId));
|
|
@@ -184,6 +184,17 @@ var provenanceCodec = {
|
|
|
184
184
|
return value;
|
|
185
185
|
}
|
|
186
186
|
};
|
|
187
|
+
var externalPersonSnapshotCodec = {
|
|
188
|
+
decode(value) {
|
|
189
|
+
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))) {
|
|
190
|
+
return err("invalid-input", "External person attribution requires a canonical person and integration-asserted evidence.");
|
|
191
|
+
}
|
|
192
|
+
return ok(value);
|
|
193
|
+
},
|
|
194
|
+
encode(value) {
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
187
198
|
|
|
188
199
|
// host-runtime.ts
|
|
189
200
|
function isRecord2(value) {
|
|
@@ -375,7 +386,7 @@ function isContribution2(value) {
|
|
|
375
386
|
if (!isRecord2(origin))
|
|
376
387
|
return false;
|
|
377
388
|
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");
|
|
378
|
-
return validOrigin && (value.latestEditor === null || snapshot(value.latestEditor));
|
|
389
|
+
return (value.personAttribution === undefined || externalPersonSnapshotCodec.decode(value.personAttribution).ok) && validOrigin && (value.latestEditor === null || snapshot(value.latestEditor));
|
|
379
390
|
}
|
|
380
391
|
function decodeEvidencePage(value, decodeItem) {
|
|
381
392
|
if (!isRecord2(value) || typeof value.status !== "string")
|
|
@@ -710,6 +721,7 @@ function createHostAdapter(options) {
|
|
|
710
721
|
});
|
|
711
722
|
const contexts = new WeakMap;
|
|
712
723
|
return {
|
|
724
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
713
725
|
pluginId: upstream.pluginId,
|
|
714
726
|
instanceId: upstream.instanceId,
|
|
715
727
|
session: (context) => upstream.session(context),
|
|
@@ -755,6 +767,7 @@ function createHostAdapter(options) {
|
|
|
755
767
|
const error = protocolError(extension);
|
|
756
768
|
const unavailableResult = async () => ({ ok: false, error });
|
|
757
769
|
return {
|
|
770
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
758
771
|
pluginId: upstream.pluginId,
|
|
759
772
|
instanceId: upstream.instanceId,
|
|
760
773
|
session: async () => ({ status: error.code === "incompatible" ? "incompatible" : "unavailable", instanceId: upstream.instanceId, error }),
|
|
@@ -830,6 +843,7 @@ function createHostAdapter(options) {
|
|
|
830
843
|
});
|
|
831
844
|
const scopes = new WeakMap;
|
|
832
845
|
return {
|
|
846
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
833
847
|
pluginId: upstream.pluginId,
|
|
834
848
|
instanceId: protocol.instanceId,
|
|
835
849
|
async session(context) {
|
|
@@ -916,6 +930,123 @@ function createHostAdapter(options) {
|
|
|
916
930
|
}
|
|
917
931
|
};
|
|
918
932
|
}
|
|
933
|
+
function decodeExternalContribution(value, strict = true) {
|
|
934
|
+
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)) || !idCodec("subject").decode(value.author.subject).ok || !isProfilePresentation(value.author.presentation)) {
|
|
935
|
+
return err("invalid-input", "Invalid V1 external contribution.");
|
|
936
|
+
}
|
|
937
|
+
let person = value.personAttribution;
|
|
938
|
+
if (!strict && isRecord2(person) && isRecord2(person.identity))
|
|
939
|
+
person = {
|
|
940
|
+
identity: { kind: person.identity.kind, key: person.identity.key, issuer: person.identity.issuer, subject: person.identity.subject },
|
|
941
|
+
presentation: isRecord2(person.presentation) ? { displayName: person.presentation.displayName, handle: person.presentation.handle, avatarUrl: person.presentation.avatarUrl } : person.presentation,
|
|
942
|
+
evidence: person.evidence
|
|
943
|
+
};
|
|
944
|
+
if (strict && isRecord2(person) && isRecord2(person.presentation) && Object.keys(person.presentation).some((key) => !["displayName", "handle", "avatarUrl"].includes(key)))
|
|
945
|
+
return err("invalid-input", "Unexpected presentation field.");
|
|
946
|
+
const decoded = person === undefined ? null : externalPersonSnapshotCodec.decode(person);
|
|
947
|
+
if (decoded && !decoded.ok)
|
|
948
|
+
return decoded;
|
|
949
|
+
return ok({
|
|
950
|
+
version: 1,
|
|
951
|
+
author: { subject: value.author.subject, presentation: { displayName: value.author.presentation.displayName, handle: value.author.presentation.handle, avatarUrl: value.author.presentation.avatarUrl } },
|
|
952
|
+
...decoded?.ok ? { personAttribution: decoded.value } : {}
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
var externalContributionV1Codec = {
|
|
956
|
+
decode: (value) => decodeExternalContribution(value),
|
|
957
|
+
encode: (value) => value
|
|
958
|
+
};
|
|
959
|
+
function canonicalExternal(value) {
|
|
960
|
+
if (Array.isArray(value))
|
|
961
|
+
return `[${value.map(canonicalExternal).join(",")}]`;
|
|
962
|
+
if (isRecord2(value))
|
|
963
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalExternal(value[key])}`).join(",")}}`;
|
|
964
|
+
return JSON.stringify(value);
|
|
965
|
+
}
|
|
966
|
+
function externalContributionMethods(upstream, extension, disposed) {
|
|
967
|
+
const support = () => {
|
|
968
|
+
if (disposed())
|
|
969
|
+
return err("disposed", "Identity host is disposed.");
|
|
970
|
+
if (extension.status === "absent")
|
|
971
|
+
return ok(null);
|
|
972
|
+
if (extension.status !== "supported")
|
|
973
|
+
return { ok: false, error: protocolError(extension) };
|
|
974
|
+
const value = extension.protocol.externalContributions;
|
|
975
|
+
if (value === undefined)
|
|
976
|
+
return ok(null);
|
|
977
|
+
if (!isRecord2(value) || !Array.isArray(value.versions) || !value.versions.length || !value.versions.every((version) => Number.isInteger(version) && version > 0))
|
|
978
|
+
return err("incompatible", "Malformed external contribution capability.");
|
|
979
|
+
if (!value.versions.includes(1))
|
|
980
|
+
return ok(null);
|
|
981
|
+
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")
|
|
982
|
+
return err("incompatible", "Malformed V1 external contribution capability.");
|
|
983
|
+
return ok(value);
|
|
984
|
+
};
|
|
985
|
+
const outcome = (raw) => {
|
|
986
|
+
const decoded = decodeAcceptance(raw);
|
|
987
|
+
if (!decoded.ok || decoded.value.status !== "submitted")
|
|
988
|
+
return decoded;
|
|
989
|
+
if (decoded.value.receipt.evidence !== "host-accepted")
|
|
990
|
+
return err("incompatible", "V1 needs durable native acceptance.");
|
|
991
|
+
const receipt = decoded.value.receipt;
|
|
992
|
+
const contribution = decodeExternalContribution(receipt.externalContribution, false);
|
|
993
|
+
return contribution.ok ? ok({ status: "submitted", receipt: { ...decoded.value.receipt, externalContribution: contribution.value } }) : err("incompatible", "Host receipt omitted or changed V1 attribution.");
|
|
994
|
+
};
|
|
995
|
+
return {
|
|
996
|
+
externalContributionCapabilities() {
|
|
997
|
+
const negotiated = support();
|
|
998
|
+
return !negotiated.ok ? negotiated : ok(negotiated.value ? { version: 1, personKinds: ["person"], history: "accepted-and-queued", rollback: "requires-capable-host" } : null);
|
|
999
|
+
},
|
|
1000
|
+
async sendExternalContribution(contribution, input, options) {
|
|
1001
|
+
const operationId = input.operationId;
|
|
1002
|
+
const decoded = decodeExternalContribution(contribution);
|
|
1003
|
+
if (!decoded.ok)
|
|
1004
|
+
return { status: "rejected", error: decoded.error };
|
|
1005
|
+
const negotiated = support();
|
|
1006
|
+
if (!negotiated.ok)
|
|
1007
|
+
return { status: "rejected", error: negotiated.error };
|
|
1008
|
+
if (!negotiated.value)
|
|
1009
|
+
return { status: "rejected", error: { code: "unsupported", message: "Native external-person attribution V1 is unavailable.", retry: "never" } };
|
|
1010
|
+
if (options?.signal?.aborted)
|
|
1011
|
+
return { status: "rejected", error: { code: "expired", message: "The send was cancelled before dispatch.", retry: "never" } };
|
|
1012
|
+
const frozen = JSON.parse(JSON.stringify(decoded.value));
|
|
1013
|
+
const wire = JSON.parse(JSON.stringify(wireInput(input, upstream.inputCodec)));
|
|
1014
|
+
try {
|
|
1015
|
+
const result = outcome(await negotiated.value.acceptV1({ contribution: frozen, input: wire }, options));
|
|
1016
|
+
if (!result.ok)
|
|
1017
|
+
return { status: "indeterminate", operationId, message: result.error.message };
|
|
1018
|
+
if (result.value.status === "submitted" && (result.value.receipt.operationId !== operationId || canonicalExternal(result.value.receipt.externalContribution) !== canonicalExternal(frozen))) {
|
|
1019
|
+
return { status: "indeterminate", operationId, message: "Host receipt did not confirm the reserved attribution." };
|
|
1020
|
+
}
|
|
1021
|
+
return result.value;
|
|
1022
|
+
} catch {
|
|
1023
|
+
return { status: "indeterminate", operationId, message: "External contribution dispatch outcome is unknown; reconcile this operation." };
|
|
1024
|
+
}
|
|
1025
|
+
},
|
|
1026
|
+
async lookupExternalContribution(operationId, options) {
|
|
1027
|
+
const negotiated = support();
|
|
1028
|
+
if (!negotiated.ok)
|
|
1029
|
+
return negotiated;
|
|
1030
|
+
if (!negotiated.value)
|
|
1031
|
+
return ok({ status: "unknown", reason: "unsupported" });
|
|
1032
|
+
try {
|
|
1033
|
+
const result = asResult(await negotiated.value.lookupV1(operationId, options));
|
|
1034
|
+
if (!result.ok)
|
|
1035
|
+
return result;
|
|
1036
|
+
if (isRecord2(result.value) && result.value.status === "final") {
|
|
1037
|
+
const decoded = outcome(result.value.outcome);
|
|
1038
|
+
if (decoded.ok && decoded.value.status === "submitted" && decoded.value.receipt.operationId !== operationId) {
|
|
1039
|
+
return err("incompatible", "Host lookup returned a receipt for another operation.");
|
|
1040
|
+
}
|
|
1041
|
+
return decoded.ok ? ok({ status: "final", outcome: decoded.value }) : decoded;
|
|
1042
|
+
}
|
|
1043
|
+
return decodeLookup(result.value);
|
|
1044
|
+
} catch {
|
|
1045
|
+
return err("unavailable", "External contribution lookup is unavailable.", "after-reconnect");
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
};
|
|
1049
|
+
}
|
|
919
1050
|
function inspectForkExtension(value) {
|
|
920
1051
|
if (value === undefined)
|
|
921
1052
|
return { status: "absent" };
|
|
@@ -928,5 +1059,6 @@ function inspectForkExtension(value) {
|
|
|
928
1059
|
export {
|
|
929
1060
|
serverSessionCodec,
|
|
930
1061
|
inspectForkExtension,
|
|
1062
|
+
externalContributionV1Codec,
|
|
931
1063
|
createHostAdapter
|
|
932
1064
|
};
|
package/dist/index-runtime.js
CHANGED
|
@@ -107,7 +107,7 @@ function isOrigin(value) {
|
|
|
107
107
|
return value.kind === "unknown" && (value.reason === "legacy" || value.reason === "upstream-unattributed" || value.reason === "missing-source");
|
|
108
108
|
}
|
|
109
109
|
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);
|
|
110
|
+
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
111
|
}
|
|
112
112
|
function isCorrelation(value) {
|
|
113
113
|
return isRecord(value) && idCodec("thread").decode(value.threadId).ok && isBoundedString(value.turnId) && isBoundedString(value.attemptId) && (value.toolCallId === null || isBoundedString(value.toolCallId));
|
|
@@ -184,6 +184,17 @@ var provenanceCodec = {
|
|
|
184
184
|
return value;
|
|
185
185
|
}
|
|
186
186
|
};
|
|
187
|
+
var externalPersonSnapshotCodec = {
|
|
188
|
+
decode(value) {
|
|
189
|
+
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))) {
|
|
190
|
+
return err("invalid-input", "External person attribution requires a canonical person and integration-asserted evidence.");
|
|
191
|
+
}
|
|
192
|
+
return ok(value);
|
|
193
|
+
},
|
|
194
|
+
encode(value) {
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
187
198
|
function defaultIdentityKey(instance) {
|
|
188
199
|
return `local:${instance}`;
|
|
189
200
|
}
|
package/dist/model-runtime.js
CHANGED
|
@@ -107,7 +107,7 @@ function isOrigin(value) {
|
|
|
107
107
|
return value.kind === "unknown" && (value.reason === "legacy" || value.reason === "upstream-unattributed" || value.reason === "missing-source");
|
|
108
108
|
}
|
|
109
109
|
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);
|
|
110
|
+
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
111
|
}
|
|
112
112
|
function isCorrelation(value) {
|
|
113
113
|
return isRecord(value) && idCodec("thread").decode(value.threadId).ok && isBoundedString(value.turnId) && isBoundedString(value.attemptId) && (value.toolCallId === null || isBoundedString(value.toolCallId));
|
|
@@ -184,6 +184,17 @@ var provenanceCodec = {
|
|
|
184
184
|
return value;
|
|
185
185
|
}
|
|
186
186
|
};
|
|
187
|
+
var externalPersonSnapshotCodec = {
|
|
188
|
+
decode(value) {
|
|
189
|
+
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))) {
|
|
190
|
+
return err("invalid-input", "External person attribution requires a canonical person and integration-asserted evidence.");
|
|
191
|
+
}
|
|
192
|
+
return ok(value);
|
|
193
|
+
},
|
|
194
|
+
encode(value) {
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
187
198
|
function defaultIdentityKey(instance) {
|
|
188
199
|
return `local:${instance}`;
|
|
189
200
|
}
|
|
@@ -202,6 +213,7 @@ export {
|
|
|
202
213
|
newConflictToken,
|
|
203
214
|
identityKeyCodec,
|
|
204
215
|
idCodec,
|
|
216
|
+
externalPersonSnapshotCodec,
|
|
205
217
|
err,
|
|
206
218
|
defaultIdentityKey
|
|
207
219
|
};
|
package/dist/server-runtime.js
CHANGED
|
@@ -187,6 +187,9 @@ function createIdentityServer(options) {
|
|
|
187
187
|
},
|
|
188
188
|
toolProvenance: (context) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.readToolProvenance(context),
|
|
189
189
|
sendExternal: (author, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternal(author, input, readOptions),
|
|
190
|
+
externalContributionCapabilities: () => disposed ? err("disposed", "Identity server is disposed.") : host.externalContributionCapabilities(),
|
|
191
|
+
sendExternalContribution: (contribution, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternalContribution(contribution, input, readOptions),
|
|
192
|
+
lookupExternalContribution: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupExternalContribution(operationId, readOptions),
|
|
190
193
|
lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupOperation(operationId, readOptions),
|
|
191
194
|
dispose
|
|
192
195
|
};
|
package/dist/testing-runtime.js
CHANGED
|
@@ -107,7 +107,7 @@ function isOrigin(value) {
|
|
|
107
107
|
return value.kind === "unknown" && (value.reason === "legacy" || value.reason === "upstream-unattributed" || value.reason === "missing-source");
|
|
108
108
|
}
|
|
109
109
|
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);
|
|
110
|
+
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
111
|
}
|
|
112
112
|
function isCorrelation(value) {
|
|
113
113
|
return isRecord(value) && idCodec("thread").decode(value.threadId).ok && isBoundedString(value.turnId) && isBoundedString(value.attemptId) && (value.toolCallId === null || isBoundedString(value.toolCallId));
|
|
@@ -184,6 +184,17 @@ var provenanceCodec = {
|
|
|
184
184
|
return value;
|
|
185
185
|
}
|
|
186
186
|
};
|
|
187
|
+
var externalPersonSnapshotCodec = {
|
|
188
|
+
decode(value) {
|
|
189
|
+
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))) {
|
|
190
|
+
return err("invalid-input", "External person attribution requires a canonical person and integration-asserted evidence.");
|
|
191
|
+
}
|
|
192
|
+
return ok(value);
|
|
193
|
+
},
|
|
194
|
+
encode(value) {
|
|
195
|
+
return value;
|
|
196
|
+
}
|
|
197
|
+
};
|
|
187
198
|
function defaultIdentityKey(instance) {
|
|
188
199
|
return `local:${instance}`;
|
|
189
200
|
}
|
|
@@ -378,7 +389,7 @@ function isContribution2(value) {
|
|
|
378
389
|
if (!isRecord2(origin))
|
|
379
390
|
return false;
|
|
380
391
|
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));
|
|
392
|
+
return (value.personAttribution === undefined || externalPersonSnapshotCodec.decode(value.personAttribution).ok) && validOrigin && (value.latestEditor === null || snapshot(value.latestEditor));
|
|
382
393
|
}
|
|
383
394
|
function decodeEvidencePage(value, decodeItem) {
|
|
384
395
|
if (!isRecord2(value) || typeof value.status !== "string")
|
|
@@ -713,6 +724,7 @@ function createHostAdapter(options) {
|
|
|
713
724
|
});
|
|
714
725
|
const contexts = new WeakMap;
|
|
715
726
|
return {
|
|
727
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
716
728
|
pluginId: upstream.pluginId,
|
|
717
729
|
instanceId: upstream.instanceId,
|
|
718
730
|
session: (context) => upstream.session(context),
|
|
@@ -758,6 +770,7 @@ function createHostAdapter(options) {
|
|
|
758
770
|
const error = protocolError(extension);
|
|
759
771
|
const unavailableResult = async () => ({ ok: false, error });
|
|
760
772
|
return {
|
|
773
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
761
774
|
pluginId: upstream.pluginId,
|
|
762
775
|
instanceId: upstream.instanceId,
|
|
763
776
|
session: async () => ({ status: error.code === "incompatible" ? "incompatible" : "unavailable", instanceId: upstream.instanceId, error }),
|
|
@@ -833,6 +846,7 @@ function createHostAdapter(options) {
|
|
|
833
846
|
});
|
|
834
847
|
const scopes = new WeakMap;
|
|
835
848
|
return {
|
|
849
|
+
...externalContributionMethods(upstream, extension, () => disposed),
|
|
836
850
|
pluginId: upstream.pluginId,
|
|
837
851
|
instanceId: protocol.instanceId,
|
|
838
852
|
async session(context) {
|
|
@@ -919,6 +933,123 @@ function createHostAdapter(options) {
|
|
|
919
933
|
}
|
|
920
934
|
};
|
|
921
935
|
}
|
|
936
|
+
function decodeExternalContribution(value, strict = true) {
|
|
937
|
+
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)) || !idCodec("subject").decode(value.author.subject).ok || !isProfilePresentation(value.author.presentation)) {
|
|
938
|
+
return err("invalid-input", "Invalid V1 external contribution.");
|
|
939
|
+
}
|
|
940
|
+
let person = value.personAttribution;
|
|
941
|
+
if (!strict && isRecord2(person) && isRecord2(person.identity))
|
|
942
|
+
person = {
|
|
943
|
+
identity: { kind: person.identity.kind, key: person.identity.key, issuer: person.identity.issuer, subject: person.identity.subject },
|
|
944
|
+
presentation: isRecord2(person.presentation) ? { displayName: person.presentation.displayName, handle: person.presentation.handle, avatarUrl: person.presentation.avatarUrl } : person.presentation,
|
|
945
|
+
evidence: person.evidence
|
|
946
|
+
};
|
|
947
|
+
if (strict && isRecord2(person) && isRecord2(person.presentation) && Object.keys(person.presentation).some((key) => !["displayName", "handle", "avatarUrl"].includes(key)))
|
|
948
|
+
return err("invalid-input", "Unexpected presentation field.");
|
|
949
|
+
const decoded = person === undefined ? null : externalPersonSnapshotCodec.decode(person);
|
|
950
|
+
if (decoded && !decoded.ok)
|
|
951
|
+
return decoded;
|
|
952
|
+
return ok({
|
|
953
|
+
version: 1,
|
|
954
|
+
author: { subject: value.author.subject, presentation: { displayName: value.author.presentation.displayName, handle: value.author.presentation.handle, avatarUrl: value.author.presentation.avatarUrl } },
|
|
955
|
+
...decoded?.ok ? { personAttribution: decoded.value } : {}
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
var externalContributionV1Codec = {
|
|
959
|
+
decode: (value) => decodeExternalContribution(value),
|
|
960
|
+
encode: (value) => value
|
|
961
|
+
};
|
|
962
|
+
function canonicalExternal(value) {
|
|
963
|
+
if (Array.isArray(value))
|
|
964
|
+
return `[${value.map(canonicalExternal).join(",")}]`;
|
|
965
|
+
if (isRecord2(value))
|
|
966
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalExternal(value[key])}`).join(",")}}`;
|
|
967
|
+
return JSON.stringify(value);
|
|
968
|
+
}
|
|
969
|
+
function externalContributionMethods(upstream, extension, disposed) {
|
|
970
|
+
const support = () => {
|
|
971
|
+
if (disposed())
|
|
972
|
+
return err("disposed", "Identity host is disposed.");
|
|
973
|
+
if (extension.status === "absent")
|
|
974
|
+
return ok(null);
|
|
975
|
+
if (extension.status !== "supported")
|
|
976
|
+
return { ok: false, error: protocolError(extension) };
|
|
977
|
+
const value = extension.protocol.externalContributions;
|
|
978
|
+
if (value === undefined)
|
|
979
|
+
return ok(null);
|
|
980
|
+
if (!isRecord2(value) || !Array.isArray(value.versions) || !value.versions.length || !value.versions.every((version) => Number.isInteger(version) && version > 0))
|
|
981
|
+
return err("incompatible", "Malformed external contribution capability.");
|
|
982
|
+
if (!value.versions.includes(1))
|
|
983
|
+
return ok(null);
|
|
984
|
+
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")
|
|
985
|
+
return err("incompatible", "Malformed V1 external contribution capability.");
|
|
986
|
+
return ok(value);
|
|
987
|
+
};
|
|
988
|
+
const outcome = (raw) => {
|
|
989
|
+
const decoded = decodeAcceptance(raw);
|
|
990
|
+
if (!decoded.ok || decoded.value.status !== "submitted")
|
|
991
|
+
return decoded;
|
|
992
|
+
if (decoded.value.receipt.evidence !== "host-accepted")
|
|
993
|
+
return err("incompatible", "V1 needs durable native acceptance.");
|
|
994
|
+
const receipt = decoded.value.receipt;
|
|
995
|
+
const contribution = decodeExternalContribution(receipt.externalContribution, false);
|
|
996
|
+
return contribution.ok ? ok({ status: "submitted", receipt: { ...decoded.value.receipt, externalContribution: contribution.value } }) : err("incompatible", "Host receipt omitted or changed V1 attribution.");
|
|
997
|
+
};
|
|
998
|
+
return {
|
|
999
|
+
externalContributionCapabilities() {
|
|
1000
|
+
const negotiated = support();
|
|
1001
|
+
return !negotiated.ok ? negotiated : ok(negotiated.value ? { version: 1, personKinds: ["person"], history: "accepted-and-queued", rollback: "requires-capable-host" } : null);
|
|
1002
|
+
},
|
|
1003
|
+
async sendExternalContribution(contribution, input, options) {
|
|
1004
|
+
const operationId = input.operationId;
|
|
1005
|
+
const decoded = decodeExternalContribution(contribution);
|
|
1006
|
+
if (!decoded.ok)
|
|
1007
|
+
return { status: "rejected", error: decoded.error };
|
|
1008
|
+
const negotiated = support();
|
|
1009
|
+
if (!negotiated.ok)
|
|
1010
|
+
return { status: "rejected", error: negotiated.error };
|
|
1011
|
+
if (!negotiated.value)
|
|
1012
|
+
return { status: "rejected", error: { code: "unsupported", message: "Native external-person attribution V1 is unavailable.", retry: "never" } };
|
|
1013
|
+
if (options?.signal?.aborted)
|
|
1014
|
+
return { status: "rejected", error: { code: "expired", message: "The send was cancelled before dispatch.", retry: "never" } };
|
|
1015
|
+
const frozen = JSON.parse(JSON.stringify(decoded.value));
|
|
1016
|
+
const wire = JSON.parse(JSON.stringify(wireInput(input, upstream.inputCodec)));
|
|
1017
|
+
try {
|
|
1018
|
+
const result = outcome(await negotiated.value.acceptV1({ contribution: frozen, input: wire }, options));
|
|
1019
|
+
if (!result.ok)
|
|
1020
|
+
return { status: "indeterminate", operationId, message: result.error.message };
|
|
1021
|
+
if (result.value.status === "submitted" && (result.value.receipt.operationId !== operationId || canonicalExternal(result.value.receipt.externalContribution) !== canonicalExternal(frozen))) {
|
|
1022
|
+
return { status: "indeterminate", operationId, message: "Host receipt did not confirm the reserved attribution." };
|
|
1023
|
+
}
|
|
1024
|
+
return result.value;
|
|
1025
|
+
} catch {
|
|
1026
|
+
return { status: "indeterminate", operationId, message: "External contribution dispatch outcome is unknown; reconcile this operation." };
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
async lookupExternalContribution(operationId, options) {
|
|
1030
|
+
const negotiated = support();
|
|
1031
|
+
if (!negotiated.ok)
|
|
1032
|
+
return negotiated;
|
|
1033
|
+
if (!negotiated.value)
|
|
1034
|
+
return ok({ status: "unknown", reason: "unsupported" });
|
|
1035
|
+
try {
|
|
1036
|
+
const result = asResult(await negotiated.value.lookupV1(operationId, options));
|
|
1037
|
+
if (!result.ok)
|
|
1038
|
+
return result;
|
|
1039
|
+
if (isRecord2(result.value) && result.value.status === "final") {
|
|
1040
|
+
const decoded = outcome(result.value.outcome);
|
|
1041
|
+
if (decoded.ok && decoded.value.status === "submitted" && decoded.value.receipt.operationId !== operationId) {
|
|
1042
|
+
return err("incompatible", "Host lookup returned a receipt for another operation.");
|
|
1043
|
+
}
|
|
1044
|
+
return decoded.ok ? ok({ status: "final", outcome: decoded.value }) : decoded;
|
|
1045
|
+
}
|
|
1046
|
+
return decodeLookup(result.value);
|
|
1047
|
+
} catch {
|
|
1048
|
+
return err("unavailable", "External contribution lookup is unavailable.", "after-reconnect");
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
922
1053
|
function inspectForkExtension(value) {
|
|
923
1054
|
if (value === undefined)
|
|
924
1055
|
return { status: "absent" };
|
|
@@ -2187,6 +2318,9 @@ function createIdentityServer(options) {
|
|
|
2187
2318
|
},
|
|
2188
2319
|
toolProvenance: (context) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.readToolProvenance(context),
|
|
2189
2320
|
sendExternal: (author, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternal(author, input, readOptions),
|
|
2321
|
+
externalContributionCapabilities: () => disposed ? err("disposed", "Identity server is disposed.") : host.externalContributionCapabilities(),
|
|
2322
|
+
sendExternalContribution: (contribution, input, readOptions) => disposed ? Promise.resolve({ status: "rejected", error: { code: "disposed", message: "Identity server is disposed.", retry: "never" } }) : host.sendExternalContribution(contribution, input, readOptions),
|
|
2323
|
+
lookupExternalContribution: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupExternalContribution(operationId, readOptions),
|
|
2190
2324
|
lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity server is disposed.")) : host.lookupOperation(operationId, readOptions),
|
|
2191
2325
|
dispose
|
|
2192
2326
|
};
|
|
@@ -2708,6 +2842,17 @@ function createBbIdentityServerBinding(options) {
|
|
|
2708
2842
|
const renderedInput = upstream.value.externalMessageRendering === "host" ? renderExternalPrompt(upstream.value.driver.pluginId, author, input.input) : input.input;
|
|
2709
2843
|
return server.sendExternal(author, { ...input, input: renderedInput }, readOptions);
|
|
2710
2844
|
},
|
|
2845
|
+
externalContributionCapabilities: () => disposed ? err("disposed", "Identity binding is disposed.") : server.externalContributionCapabilities(),
|
|
2846
|
+
async sendExternalContribution(contribution, input, readOptions) {
|
|
2847
|
+
if (disposed)
|
|
2848
|
+
return { status: "rejected", error: { code: "disposed", message: "Identity binding is disposed.", retry: "never" } };
|
|
2849
|
+
const decoded = externalContributionV1Codec.decode(contribution);
|
|
2850
|
+
if (!decoded.ok)
|
|
2851
|
+
return { status: "rejected", error: decoded.error };
|
|
2852
|
+
const renderedInput = upstream.value.externalMessageRendering === "host" ? renderExternalPrompt(upstream.value.driver.pluginId, decoded.value.author, input.input) : input.input;
|
|
2853
|
+
return server.sendExternalContribution(decoded.value, { ...input, input: renderedInput }, readOptions);
|
|
2854
|
+
},
|
|
2855
|
+
lookupExternalContribution: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.lookupExternalContribution(operationId, readOptions),
|
|
2711
2856
|
lookupOperation: (operationId, readOptions) => disposed ? Promise.resolve(err("disposed", "Identity binding is disposed.")) : server.lookupOperation(operationId, readOptions),
|
|
2712
2857
|
history: guardedHistory
|
|
2713
2858
|
};
|
package/host.d.ts
CHANGED
|
@@ -37,6 +37,29 @@ 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
|
+
/** Context-free support; native storage written by V1 requires a capable host on rollback. */
|
|
53
|
+
export interface ExternalContributionCapabilityV1 {
|
|
54
|
+
readonly version: 1; readonly personKinds: readonly ['person'];
|
|
55
|
+
readonly history: 'accepted-and-queued'; readonly rollback: 'requires-capable-host';
|
|
56
|
+
}
|
|
57
|
+
export interface ExternalContributionProtocolV1<Input> {
|
|
58
|
+
readonly versions: readonly number[]; readonly personKinds: readonly ['person'];
|
|
59
|
+
readonly history: 'accepted-and-queued'; readonly rollback: 'requires-capable-host';
|
|
60
|
+
acceptV1(request: { readonly contribution: Wire<ExternalContributionV1>; readonly input: Wire<SendInput<Input>> }, options?: ReadOptions): Promise<Wire<ExternalContributionOutcomeV1>>;
|
|
61
|
+
lookupV1(operationId: string, options?: ReadOptions): Promise<Wire<Result<OperationLookup<ExternalContributionOutcomeV1>>>>;
|
|
62
|
+
}
|
|
40
63
|
export interface IdentityHost<RequestContext, ToolContext, Input> extends Disposable {
|
|
41
64
|
readonly pluginId: PluginId;
|
|
42
65
|
readonly instanceId: InstanceId;
|
|
@@ -51,6 +74,10 @@ export interface IdentityHost<RequestContext, ToolContext, Input> extends Dispos
|
|
|
51
74
|
/** Checks synchronously before upstream dispatch; enhanced host checks again inside acceptance. */
|
|
52
75
|
send(request: LiveRequest, input: SendInput<Input>, options?: ReadOptions): Promise<AcceptanceOutcome>;
|
|
53
76
|
sendExternal(author: ExternalAuthorInput, input: SendInput<Input>, options?: ReadOptions): Promise<AcceptanceOutcome>;
|
|
77
|
+
externalContributionCapabilities(): Result<ExternalContributionCapabilityV1 | null>;
|
|
78
|
+
sendExternalContribution(contribution: ExternalContributionV1, input: SendInput<Input>, options?: ReadOptions): Promise<ExternalContributionOutcomeV1>;
|
|
79
|
+
lookupExternalContribution(operationId: SendInput<Input>['operationId'], options?: ReadOptions): Promise<Result<OperationLookup<ExternalContributionOutcomeV1>>>;
|
|
80
|
+
|
|
54
81
|
lookupOperation(operationId: SendInput<Input>['operationId'], options?: ReadOptions): Promise<Result<OperationLookup<AcceptanceOutcome>>>;
|
|
55
82
|
forward(request: LiveRequest, destinationPlugin: string, method: string, input: import('./model.js').Json): Promise<unknown>;
|
|
56
83
|
registerProvider(provider: IdentityProvider): Promise<Result<ProviderRegistrationV1>>;
|
|
@@ -149,6 +176,7 @@ export interface ForkIdentityProvider {
|
|
|
149
176
|
/** Core implements these host-owned operations, not IdentityHost's fallback/directory composition. */
|
|
150
177
|
export interface ForkIdentityProtocolV1<RequestContext, ToolContext, Input> {
|
|
151
178
|
readonly version: 1;
|
|
179
|
+
readonly externalContributions?: ExternalContributionProtocolV1<Input>;
|
|
152
180
|
/** Persisted core storage namespace; stable across plugin replacement and never request-derived. */
|
|
153
181
|
readonly instanceId: string;
|
|
154
182
|
/** 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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phosphorco/bb-identity",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Portable identity, provenance, and state contracts for BB integrations.",
|
|
@@ -76,7 +76,7 @@
|
|
|
76
76
|
"access": "public"
|
|
77
77
|
},
|
|
78
78
|
"dependencies": {
|
|
79
|
-
"zod": "4.
|
|
79
|
+
"zod": "4.6.7"
|
|
80
80
|
},
|
|
81
81
|
"peerDependencies": {
|
|
82
82
|
"@get-bb/plugin-sdk": "^0.4.15 || ^0.5.9 || ^0.6.29",
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
}
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
|
-
"@get-bb/plugin-sdk": "0.
|
|
94
|
+
"@get-bb/plugin-sdk": "0.6.52",
|
|
95
95
|
"@playwright/test": "1.63.0",
|
|
96
96
|
"@types/bun": "1.3.14",
|
|
97
97
|
"@types/react": "19.2.2",
|
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
|
}
|