twin3-sdk 0.2.17-pre-alpha → 0.2.19-pre-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -116,22 +116,24 @@ pip install -e /path/to/twin3-sdk
116
116
  * Any protocol or algorithm improvement in `twin3-sdk` is reflected in both applications in **0 milliseconds**.
117
117
 
118
118
  #### 2. Production Distribution
119
- `v0.2.17-pre-alpha` is the current release candidate: `0.2.17-pre-alpha` for npm and `0.2.17a0` for PyPI. It is published from main through the Cloud Build tag trigger; the previous version number was never published. Consumers must pin the exact released version and lock its registry integrity; never use a mutable range for a cryptographic protocol dependency. This release stays pre-alpha. Marketplace or sales copy may offer it for evaluation; it is not a stable release, and the npm dist-tag is `next`, not `latest`.
119
+ `v0.2.19-pre-alpha` is the current release candidate: `0.2.19-pre-alpha` for npm and `0.2.19a0` for PyPI. It is published from reviewed main through the existing Cloud Build tag trigger. Consumers must pin the exact released version and lock its registry integrity; never use a mutable range for a cryptographic protocol dependency. This release stays pre-alpha. Marketplace or sales copy may offer it for evaluation; it is not a stable release, and the npm dist-tag is `next`, not `latest`.
120
+
121
+ The additive `twin3-sdk/runtime` subpath provides a single-action Agent runtime candidate. Products retain authenticated authority, context egress consent, durable storage and trusted tool brokers. Python `twin3.runtime` provides compatible contracts and digests only. OpenShell, ADK and Hermes adapters are not included. See [runtime scope and limits](docs/architecture/PERSONAL-AGENT-RUNTIME-V1.md).
120
122
 
121
123
  `package.json` `engines` requires Node.js `>=22.23.4` and does not set an npm engine. CI covers patched Node 22, 24 and 26; Node 18 and 20 are outside the supported baseline. Packing this repository was checked with npm 10 or newer; the stricter npm version in the release runbook applies only to Trusted Publishing.
122
124
 
123
125
  After publication, install the exact released version from the registry:
124
126
 
125
127
  ```bash
126
- npm install twin3-sdk@0.2.17-pre-alpha
127
- pip install twin3-sdk==0.2.17a0
128
+ npm install twin3-sdk@0.2.19-pre-alpha
129
+ pip install twin3-sdk==0.2.19a0
128
130
  ```
129
131
 
130
132
  For local evaluation, you can also use the packed npm tarball. Do not install from a git URL or a version range:
131
133
 
132
134
  ```bash
133
135
  npm pack --ignore-scripts
134
- npm install ./twin3-sdk-0.2.17-pre-alpha.tgz
136
+ npm install ./twin3-sdk-0.2.19-pre-alpha.tgz
135
137
  ```
136
138
 
137
139
  ---
package/index.d.ts CHANGED
@@ -223,6 +223,9 @@ export interface EngineObservationReading {
223
223
  }
224
224
  export declare function normalizeEngineObservation(row: EngineObservationInput, options: { host: string }): EngineObservation;
225
225
  export declare function summarizeEngineObservations(observations: EngineObservation[], options: { host: string; sourceLevel?: boolean }): EngineObservationReading;
226
+ export declare function summarizeCohortVisibility(readingsBySubject: Record<string, EngineObservationReading>, subjectId: string, options?: { minCohortSize?: number }): Record<string, any>;
227
+ export declare function buildSignedReferral(options: Record<string, any>): Record<string, any>;
228
+ export declare function verifySignedReferral(token: Record<string, any>, options: Record<string, any>): Record<string, any>;
226
229
 
227
230
  export declare const DESCRIPTOR_CANONICALIZATION: "rfc8785";
228
231
  export declare const DESCRIPTOR_SIGNATURE_FIELDS: readonly ["signatures", "signatureStatus"];
@@ -1708,6 +1711,16 @@ export declare class TransitionError extends Error {
1708
1711
  export declare function emptyTaskStore(): { schema: typeof TASK_STORE_SCHEMA; owners: Record<string, any> };
1709
1712
  export declare function normalizeTaskStore(value: unknown): Record<string, any>;
1710
1713
  export declare class TaskInbox {
1714
+ readIssuerCompletions(envelope: Envelope, options?: { limit?: number; cursor?: [number, string] | null; now?: number | null }): {
1715
+ tasks: TaskInboxTask[]; next_cursor: [number, string] | null;
1716
+ };
1717
+ applyCompletionPolicy(taskId: string, policy: CompletionPolicy, envelope: Envelope,
1718
+ options?: { now?: number | null; acceptedAt?: number | null }): TaskInboxTask & { completion: CompletionJournal };
1719
+ readCompletion(taskId: string, envelope: Envelope, options?: { now?: number | null }): TaskInboxTask;
1720
+ applyCompletionReport(taskId: string, command: CompletionCommand, envelope: Envelope,
1721
+ options?: { now?: number | null }): TaskInboxTask & { completion: CompletionJournal };
1722
+ applyCompletion(taskId: string, command: CompletionCommand, actor: string,
1723
+ options?: { verifier?: string | null; now?: number | null }): TaskInboxTask & { completion: CompletionJournal };
1711
1724
  constructor(taskStore: Record<string, any>, counterparties: CounterpartyTable, owner: string,
1712
1725
  options?: Omit<TaskInboxOptions, "store" | "taskStore" | "counterparties" | "owner">);
1713
1726
  constructor(options: TaskInboxOptions & {
@@ -1950,3 +1963,63 @@ export declare function verifyConsumerRegistration(statement: ConsumerRegistrati
1950
1963
  rootPublicKey: string | Buffer | import("node:crypto").KeyObject; expectedKid?: string | null; now: number;
1951
1964
  expected?: ConsumerRegistrationExpectation | null;
1952
1965
  }): VerifiedConsumerRegistration;
1966
+ export type CompletionMode = "manual" | "platform" | "api";
1967
+ export type CompletionFormat = "text" | "link" | "image" | "pdf";
1968
+ export type CompletionStatus = "awaiting_submission" | "pending_review" | "pending_check" | "awaiting_report" | "completed" | "rejected" | "unverified";
1969
+ export interface CompletionRules {
1970
+ mode: CompletionMode;
1971
+ conditions: Record<string, unknown>;
1972
+ evidence_required: boolean;
1973
+ evidence_formats: CompletionFormat[];
1974
+ allow_resubmit: boolean;
1975
+ }
1976
+ export interface CompletionEvidence { kind: CompletionFormat; reference: string; }
1977
+ export type CompletionCommand = { operation_id: string; expected_revision: number } & (
1978
+ { action: "configure"; rules: CompletionRules } |
1979
+ { action: "submit"; evidence: CompletionEvidence[] } |
1980
+ { action: "decide"; outcome: "passed" | "failed" | "unavailable"; reason: string }
1981
+ );
1982
+ export interface CompletionSubmission { evidence: CompletionEvidence[]; submitted_at: number; revision: number; }
1983
+ export interface CompletionEvent {
1984
+ policy?: CompletionPolicyReference;
1985
+ revision: number;
1986
+ rules_version: number;
1987
+ actor: string;
1988
+ at: number;
1989
+ command: CompletionCommand;
1990
+ status: CompletionStatus;
1991
+ submission: CompletionSubmission | null;
1992
+ }
1993
+ export interface CompletionJournal {
1994
+ policy?: CompletionPolicyReference;
1995
+ revision: number;
1996
+ rules_version: number;
1997
+ rules: CompletionRules | null;
1998
+ status: CompletionStatus;
1999
+ submission: CompletionSubmission | null;
2000
+ history: CompletionEvent[];
2001
+ operations: Record<string, { actor: string; verifier: string | null; command: CompletionCommand }>;
2002
+ }
2003
+ export declare class CompletionError extends Error {}
2004
+ export interface CompletionPolicy { version: number; operation_id: string; rules: CompletionRules; }
2005
+ export interface CompletionPolicyReference { version: number; operation_id: string; digest: string; }
2006
+ export interface CompletionOfferScope {
2007
+ kind: Envelope["kind"];
2008
+ signature_verified: boolean;
2009
+ sender_key: string;
2010
+ sender_key_id: string | null;
2011
+ envelope_id: string;
2012
+ envelope_digest: string;
2013
+ }
2014
+ export declare function completionPolicyPayload(task: CompletionOfferScope, policy: CompletionPolicy): {
2015
+ scope: "task_offer_completion_all_recipients";
2016
+ sender_key: string; sender_key_id: string; envelope_id: string; envelope_digest: string; policy_digest: string;
2017
+ };
2018
+ export declare function completionIssuerReadPayload(): { scope: "task_completion_issuer_read" };
2019
+ export declare function completionReportPayload(task: { task_id: string; owner: string }, command: CompletionCommand | { action: "read" }): {
2020
+ task_id: string; owner: string; completion_digest: string;
2021
+ };
2022
+ export declare function applyTaskCompletion<T extends { task_id: string; owner: string; sender_key: string; completion?: CompletionJournal }>(
2023
+ task: T, command: CompletionCommand, actor: string,
2024
+ options?: { verifier?: string | null; now?: number }
2025
+ ): T & { completion: CompletionJournal };
@@ -0,0 +1,157 @@
1
+ "use strict";
2
+
3
+ class CompletionError extends Error {}
4
+
5
+ function fail(code) { throw new CompletionError(code); }
6
+ function object(value) { return value !== null && typeof value === "object" && !Array.isArray(value); }
7
+ function identifier(value) { return typeof value === "string" && [...value].length >= 1 && [...value].length <= 256 && value === value.trim(); }
8
+ function keys(value, expected) { return object(value) && Object.keys(value).sort().join(",") === [...expected].sort().join(","); }
9
+ function clone(value) { return JSON.parse(JSON.stringify(value)); }
10
+ function equal(a, b) {
11
+ if (a === b) return true;
12
+ if (Array.isArray(a) && Array.isArray(b)) return a.length === b.length && a.every((v, i) => equal(v, b[i]));
13
+ if (!object(a) || !object(b) || !keys(a, Object.keys(b))) return false;
14
+ return Object.keys(a).every(k => equal(a[k], b[k]));
15
+ }
16
+ function jsonValue(value) {
17
+ if (value === null || typeof value === "string" || typeof value === "boolean") return true;
18
+ if (typeof value === "number") return Number.isFinite(value);
19
+ if (Array.isArray(value)) return value.every(jsonValue);
20
+ return object(value) && Object.getPrototypeOf(value) === Object.prototype && Object.values(value).every(jsonValue);
21
+ }
22
+ function rules(value) {
23
+ if (!keys(value, ["mode", "conditions", "evidence_required", "evidence_formats", "allow_resubmit"])) fail("invalid_completion_rules");
24
+ if (!["manual", "platform", "api"].includes(value.mode)
25
+ || typeof value.evidence_required !== "boolean" || typeof value.allow_resubmit !== "boolean") fail("invalid_completion_rules");
26
+ const formats = value.evidence_formats;
27
+ if (!Array.isArray(formats) || new Set(formats).size !== formats.length
28
+ || formats.some(kind => !["text", "link", "image", "pdf"].includes(kind))
29
+ || (value.evidence_required && !formats.length)
30
+ || !object(value.conditions) || !Object.keys(value.conditions).length) fail("invalid_completion_rules");
31
+ return clone(value);
32
+ }
33
+ function evidence(value, policy) {
34
+ if (!Array.isArray(value) || value.length > 20) fail("invalid_completion_evidence");
35
+ if (policy.evidence_required && !value.length) fail("completion_evidence_required");
36
+ for (const item of value) {
37
+ if (!keys(item, ["kind", "reference"]) || !policy.evidence_formats.includes(item.kind) || !identifier(item.reference)) fail("invalid_completion_evidence");
38
+ }
39
+ return clone(value);
40
+ }
41
+ function pending(policy) { return { manual: "pending_review", platform: "pending_check", api: "awaiting_report" }[policy.mode]; }
42
+
43
+ function applyTaskCompletion(task, command, actor, { verifier = null, now = Date.now() / 1000 } = {}) {
44
+ if (!object(task) || !["task_id", "owner", "sender_key"].every(key => identifier(task[key]))) fail("invalid_completion_task");
45
+ if (!identifier(actor) || ![task.owner, task.sender_key, verifier].includes(actor)) fail("completion_actor_forbidden");
46
+ if (verifier !== null && (!identifier(verifier) || [task.owner, task.sender_key].includes(verifier))) fail("invalid_completion_verifier");
47
+ const fields = { configure: ["rules"], submit: ["evidence"], decide: ["outcome", "reason"] };
48
+ if (!object(command) || !Object.hasOwn(fields, command.action)
49
+ || !keys(command, ["action", "operation_id", "expected_revision", ...fields[command.action]])) fail("invalid_completion_command");
50
+ if (!identifier(command.operation_id)) fail("completion_operation_required");
51
+ const expected = command.expected_revision;
52
+ if (!Number.isSafeInteger(expected) || expected < 0) fail("invalid_completion_revision");
53
+ if (typeof now !== "number" || !Number.isFinite(now) || now < 0) fail("invalid_completion_time");
54
+ try {
55
+ if (!jsonValue(command)) fail("invalid_completion_command");
56
+ if (Buffer.byteLength(JSON.stringify(command)) > 65536) fail("completion_command_too_large");
57
+ } catch (error) {
58
+ if (error instanceof CompletionError) throw error;
59
+ fail("invalid_completion_command");
60
+ }
61
+ const result = clone(task);
62
+ const journal = result.completion ||= { revision: 0, rules_version: 0, rules: null,
63
+ status: "awaiting_submission", submission: null, history: [], operations: {} };
64
+ const operation = command.operation_id;
65
+ const payload = { actor, verifier, command };
66
+ if (Object.hasOwn(journal.operations, operation)) {
67
+ if (!equal(journal.operations[operation], payload)) fail("completion_idempotency_conflict");
68
+ return result;
69
+ }
70
+ if (expected !== journal.revision) fail("stale_completion_revision");
71
+ if (journal.history.length && now < journal.history.at(-1).at) fail("invalid_completion_time");
72
+ if (command.action === "configure") {
73
+ if (actor !== task.sender_key) fail("completion_issuer_required");
74
+ if (journal.policy) fail("completion_policy_required");
75
+ journal.rules = rules(command.rules);
76
+ journal.rules_version += 1;
77
+ journal.status = journal.submission !== null ? pending(journal.rules) : "awaiting_submission";
78
+ } else {
79
+ const policy = journal.rules;
80
+ if (policy === null) fail("completion_rules_required");
81
+ if (command.action === "submit") {
82
+ if (actor !== task.owner) fail("completion_participant_required");
83
+ if (journal.status === "completed") fail("completion_already_completed");
84
+ if (journal.status === "rejected" && !policy.allow_resubmit) fail("completion_resubmission_forbidden");
85
+ journal.submission = { evidence: evidence(command.evidence, policy), submitted_at: now, revision: expected + 1 };
86
+ journal.status = pending(policy);
87
+ } else {
88
+ const reviewer = policy.mode === "platform" ? verifier : task.sender_key;
89
+ if (reviewer === null || actor !== reviewer) fail("completion_reviewer_forbidden");
90
+ if (journal.submission === null) fail("completion_submission_required");
91
+ if (["completed", "rejected"].includes(journal.status)) fail("completion_decision_final");
92
+ if (!["passed", "failed", "unavailable"].includes(command.outcome) || !identifier(command.reason)) fail("invalid_completion_decision");
93
+ if (command.outcome === "passed") evidence(journal.submission.evidence, policy);
94
+ journal.status = { passed: "completed", failed: "rejected", unavailable: "unverified" }[command.outcome];
95
+ }
96
+ }
97
+ journal.revision += 1;
98
+ journal.history.push({ revision: journal.revision, rules_version: journal.rules_version, actor,
99
+ at: now, command: clone(command), status: journal.status, submission: clone(journal.submission) });
100
+ Object.defineProperty(journal.operations, operation, { value: clone(payload), enumerable: true, configurable: true, writable: true });
101
+ return result;
102
+ }
103
+
104
+ function completionIssuerReadPayload() {
105
+ return { scope: "task_completion_issuer_read" };
106
+ }
107
+
108
+ function completionReportPayload(task, command) {
109
+ if (!object(task) || !identifier(task.task_id) || !identifier(task.owner)) fail("invalid_completion_task");
110
+ if (!object(command) || !jsonValue(command)) fail("invalid_completion_command");
111
+ const core = require("../internal/core.js");
112
+ return { task_id: task.task_id, owner: task.owner,
113
+ completion_digest: core.crypto.createHash("sha256").update(core.canonicalJsonBuffer(command)).digest("hex") };
114
+ }
115
+
116
+ function completionPolicyPayload(task, policy) {
117
+ if (!object(task) || task.kind !== "task_offer" || task.signature_verified !== true
118
+ || !["sender_key", "sender_key_id", "envelope_id", "envelope_digest"].every(key => identifier(task[key]))) fail("invalid_completion_offer");
119
+ if (!keys(policy, ["version", "operation_id", "rules"]) || !Number.isSafeInteger(policy.version) || policy.version < 1
120
+ || !identifier(policy.operation_id) || [...policy.operation_id].length > 128) fail("invalid_completion_policy");
121
+ try { if (!jsonValue(policy)) fail("invalid_completion_policy"); }
122
+ catch { fail("invalid_completion_policy"); }
123
+ rules(policy.rules);
124
+ if (Buffer.byteLength(JSON.stringify(policy)) > 65536) fail("completion_command_too_large");
125
+ const core = require("../internal/core.js");
126
+ const payload = { scope: "task_offer_completion_all_recipients", sender_key: task.sender_key,
127
+ sender_key_id: task.sender_key_id, envelope_id: task.envelope_id, envelope_digest: task.envelope_digest,
128
+ policy_digest: core.crypto.createHash("sha256").update(core.canonicalJsonBuffer(policy)).digest("hex") };
129
+ return payload;
130
+ }
131
+
132
+ function applyCompletionPolicy(task, policy, actor, { now = Date.now() / 1000 } = {}) {
133
+ const payload = completionPolicyPayload(task, policy);
134
+ if (actor !== task.sender_key) fail("completion_issuer_required");
135
+ const core = require("../internal/core.js");
136
+ const digest = core.crypto.createHash("sha256").update(core.canonicalJsonBuffer(payload)).digest("hex");
137
+ const previous = task.completion?.policy;
138
+ const applied = (task.completion?.history || []).find(event => event.policy?.operation_id === policy.operation_id);
139
+ if (applied) {
140
+ if (!core.secureEquals(applied.policy.digest, digest)) fail("completion_policy_conflict");
141
+ return clone(task);
142
+ }
143
+ if (previous && policy.version < previous.version) fail("stale_completion_policy");
144
+ if (previous && policy.version === previous.version) {
145
+ if (!core.secureEquals(previous.digest, digest)) fail("completion_policy_conflict");
146
+ return clone(task);
147
+ }
148
+ const input = clone(task);
149
+ if (input.completion) delete input.completion.policy;
150
+ const result = applyTaskCompletion(input, { action: "configure", operation_id: `policy:${policy.version}:${policy.operation_id}`,
151
+ expected_revision: input.completion?.revision || 0, rules: policy.rules }, actor, { now });
152
+ result.completion.policy = { version: policy.version, operation_id: policy.operation_id, digest };
153
+ result.completion.history.at(-1).policy = clone(result.completion.policy);
154
+ return result;
155
+ }
156
+
157
+ module.exports = { CompletionError, applyTaskCompletion, completionReportPayload, completionPolicyPayload, completionIssuerReadPayload, applyCompletionPolicy };
@@ -250,10 +250,20 @@ function envelopeDigest(envelope) {
250
250
  return crypto.createHash("sha256").update(canonicalJsonBuffer(body)).digest("hex");
251
251
  }
252
252
 
253
- function buildEnvelope(identity, kind, payload, { ttlSeconds = 900, now = Math.floor(Date.now() / 1000), legacySchemas = false } = {}) {
253
+ function buildEnvelope(identity, kind, payload, { ttlSeconds = 900, now = Math.floor(Date.now() / 1000), legacySchemas = false, attribution = undefined } = {}) {
254
254
  const sender = identityOf(identity);
255
255
  if (!ENVELOPE_KINDS.has(kind)) throw sdkError("envelope_kind_unknown");
256
256
  if (!payload || typeof payload !== "object" || Array.isArray(payload) || Object.keys(payload).length === 0) throw sdkError("envelope_payload_empty");
257
+ if (attribution !== undefined && attribution !== null) {
258
+ if (!attribution || typeof attribution !== "object" || Array.isArray(attribution)
259
+ || Object.keys(attribution).length !== 2
260
+ || !Object.prototype.hasOwnProperty.call(attribution, "attributed_card_digest")
261
+ || !Object.prototype.hasOwnProperty.call(attribution, "attribution_source")
262
+ || (attribution.attributed_card_digest !== null && (typeof attribution.attributed_card_digest !== "string" || !/^[0-9a-f]{64}$/.test(attribution.attributed_card_digest)))
263
+ || !["card_endpoint", "referral_token", "unknown"].includes(attribution.attribution_source)) {
264
+ throw sdkError("envelope_attribution_invalid");
265
+ }
266
+ }
257
267
  const at = Math.floor(now);
258
268
  const envelope = {
259
269
  schema: legacySchemas ? LEGACY.envelope : ENVELOPE_SCHEMA,
@@ -267,6 +277,9 @@ function buildEnvelope(identity, kind, payload, { ttlSeconds = 900, now = Math.f
267
277
  issued_at: at,
268
278
  deadline: at + Math.floor(ttlSeconds),
269
279
  };
280
+ if (attribution !== undefined && attribution !== null) {
281
+ envelope.attribution = JSON.parse(JSON.stringify(attribution));
282
+ }
270
283
  void sender;
271
284
  envelope.digest = envelopeDigest(envelope);
272
285
  return envelope;
package/js/comm.d.ts CHANGED
@@ -1,3 +1,9 @@
1
+ export {
2
+ CompletionMode, CompletionFormat, CompletionStatus, CompletionRules, CompletionEvidence,
3
+ CompletionCommand, CompletionSubmission, CompletionEvent, CompletionJournal,
4
+ CompletionError, completionReportPayload, applyTaskCompletion,
5
+ CompletionPolicy, CompletionPolicyReference, CompletionOfferScope, completionPolicyPayload, completionIssuerReadPayload
6
+ } from "../index";
1
7
  export {
2
8
  ACCESS_GRANT_SCHEMA,
3
9
  COUNTERPARTY_RECORD_SCHEMA,
package/js/comm.js CHANGED
@@ -1,7 +1,13 @@
1
1
  "use strict";
2
2
  const impl = require("./internal/comm.js");
3
+ const completion = require("./comm/completion.js");
3
4
 
4
5
  module.exports = {
6
+ CompletionError: completion.CompletionError,
7
+ applyTaskCompletion: completion.applyTaskCompletion,
8
+ completionReportPayload: completion.completionReportPayload,
9
+ completionPolicyPayload: completion.completionPolicyPayload,
10
+ completionIssuerReadPayload: completion.completionIssuerReadPayload,
5
11
  TaskInbox: impl.TaskInbox,
6
12
  IntakeRejected: impl.IntakeRejected,
7
13
  TransitionError: impl.TransitionError,
package/js/comm.mjs CHANGED
@@ -1,4 +1,9 @@
1
1
  import api from "./comm.js";
2
+ export const CompletionError = api.CompletionError;
3
+ export const applyTaskCompletion = api.applyTaskCompletion;
4
+ export const completionReportPayload = api.completionReportPayload;
5
+ export const completionPolicyPayload = api.completionPolicyPayload;
6
+ export const completionIssuerReadPayload = api.completionIssuerReadPayload;
2
7
  export const TaskInbox = api.TaskInbox;
3
8
  export const IntakeRejected = api.IntakeRejected;
4
9
  export const TransitionError = api.TransitionError;
@@ -2,12 +2,14 @@
2
2
 
3
3
  const crypto = require("node:crypto");
4
4
  const zhTw = require("../twin3/discovery/evidence-i18n.json")["zh-TW"];
5
+ const { validId } = require("./id-format.js");
5
6
 
6
7
  const VISIT_EVIDENCE_SCHEMA = "twin3-discovery-visit-evidence-v1";
7
8
  const AI_EXPOSURE_SCHEMA = "twin3-discovery-ai-exposure-v1";
8
9
  const DISCOVERY_SUMMARY_SCHEMA = "twin3-discovery-evidence-summary-v1";
9
10
  const DISCOVERY_VIEW_SCHEMA = "twin3-discovery-evidence-view-v1";
10
11
  const VISIT_FIELDS = ["schema", "host", "path", "surface", "source", "record_id", "source_digest", "observed_at", "request_count", "method", "status", "provider", "bot", "evidence_level", "user_agent_digest", "digest"];
12
+ const CARD_ATTRIBUTION_FIELDS = ["subject_namespace", "subject_id", "card_digest", "claim_axis", "network_axis", "identity_axis"];
11
13
  const EXPOSURE_FIELDS = ["schema", "host", "source", "report_digest", "date", "page", "page_digest", "impressions", "evidence_level", "digest"];
12
14
  const SHA256 = /^[0-9a-f]{64}$/;
13
15
  const URL_COMPONENT = /^(?:[A-Za-z0-9._~!$&'()*+,;=:@/?-]|%[0-9A-Fa-f]{2})*$/;
@@ -28,6 +30,14 @@ const BOTS = [
28
30
  ["Bingbot", "Microsoft"],
29
31
  ["Applebot", "Apple"],
30
32
  ];
33
+ const PROTOCOL_BOTS = [
34
+ ["A2A-Client", "A2A"],
35
+ ["WebMCP-Client", "WebMCP"],
36
+ ];
37
+ const ALL_BOTS = [...BOTS, ...PROTOCOL_BOTS];
38
+ const CLAIM_AXES = ["unclassified", "ua_claim", "protocol_claim"];
39
+ const NETWORK_AXES = ["unverified", "published_ip_range_matched", "rdns_verified"];
40
+ const IDENTITY_AXES = ["unsigned", "request_signature_valid", "kya_delegation_bound"];
31
41
 
32
42
  class DiscoveryEvidenceError extends Error {
33
43
  constructor(code) {
@@ -97,7 +107,35 @@ function pathValue(value) {
97
107
  return path;
98
108
  }
99
109
 
100
- function visitFields(row, ownerHost) {
110
+ function cardAttributionInput(row) {
111
+ const hasAttribution = CARD_ATTRIBUTION_FIELDS.some(key => Object.hasOwn(row, key)) || row.surface === "hosted_card";
112
+ if (!hasAttribution) return null;
113
+ const namespace = Object.hasOwn(row, "subject_namespace") ? row.subject_namespace : null;
114
+ const subjectId = Object.hasOwn(row, "subject_id") ? row.subject_id : null;
115
+ requireValue((namespace === null) === (subjectId === null), "visit_subject_invalid");
116
+ if (namespace !== null) {
117
+ requireValue(namespace === "agt" || namespace === "hum", "visit_subject_invalid");
118
+ requireValue(typeof subjectId === "string" && validId(subjectId) && subjectId.startsWith(namespace + "_") && subjectId.length > 4, "visit_subject_invalid");
119
+ }
120
+ const cardDigest = Object.hasOwn(row, "card_digest") ? row.card_digest : null;
121
+ requireValue(cardDigest === null || isSha256(cardDigest), "visit_card_digest_invalid");
122
+ const networkAxis = row.network_axis === undefined ? "unverified" : row.network_axis;
123
+ requireValue(NETWORK_AXES.includes(networkAxis), "visit_network_axis_invalid");
124
+ const identityAxis = row.identity_axis === undefined ? "unsigned" : row.identity_axis;
125
+ requireValue(IDENTITY_AXES.includes(identityAxis), "visit_identity_axis_invalid");
126
+ const claimAxis = Object.hasOwn(row, "claim_axis") ? row.claim_axis : null;
127
+ requireValue(claimAxis === null || CLAIM_AXES.includes(claimAxis), "visit_claim_axis_invalid");
128
+ return {
129
+ subject_namespace: namespace,
130
+ subject_id: subjectId,
131
+ card_digest: cardDigest,
132
+ claim_axis: claimAxis,
133
+ network_axis: networkAxis,
134
+ identity_axis: identityAxis,
135
+ };
136
+ }
137
+
138
+ function visitFields(row, ownerHost, { allowHostedCard = false } = {}) {
101
139
  requireValue(["cdn_log", "server_log"].includes(row.source), "visit_source_invalid");
102
140
  requireValue(typeof row.record_id === "string" && row.record_id.length > 0 && row.record_id.length <= 160 && !/[^A-Za-z0-9._:-]/.test(row.record_id), "visit_record_id_required");
103
141
  requireValue(isSha256(row.source_digest), "visit_source_digest_invalid");
@@ -109,8 +147,10 @@ function visitFields(row, ownerHost) {
109
147
  requireValue(method === "GET" || method === "HEAD", "visit_method_invalid");
110
148
  const path = pathValue(row.path);
111
149
  if (Object.hasOwn(row, "host")) requireValue(hostValue(row.host) === ownerHost, "visit_host_mismatch");
150
+ const defaultSurface = ["/.well-known/agent-card.json", "/.well-known/agent.json"].includes(path) ? "agent_card" : "website";
151
+ const surface = allowHostedCard && row.surface === "hosted_card" ? "hosted_card" : defaultSurface;
112
152
  return { schema: VISIT_EVIDENCE_SCHEMA, host: ownerHost, path,
113
- surface: ["/.well-known/agent-card.json", "/.well-known/agent.json"].includes(path) ? "agent_card" : "website",
153
+ surface,
114
154
  source: row.source, record_id: row.record_id, source_digest: row.source_digest,
115
155
  observed_at: row.observed_at, request_count: requestCount, method, status };
116
156
  }
@@ -137,17 +177,35 @@ function createDiscoveryEvidenceApi(canonicalJsonBuffer) {
137
177
  function normalizeDiscoveryVisit(row, { host } = {}) {
138
178
  requireValue(isRecord(row), "visit_invalid");
139
179
  const ownerHost = hostValue(host);
140
- const fields = visitFields(row, ownerHost);
180
+ const attribution = cardAttributionInput(row);
181
+ const fields = visitFields(row, ownerHost, { allowHostedCard: attribution !== null });
141
182
  const userAgent = row.user_agent === undefined ? "" : row.user_agent;
142
183
  requireValue(typeof userAgent === "string" && [...userAgent].length <= 512 && !/[\ud800-\udfff]/u.test(userAgent), "visit_user_agent_invalid");
143
- const bot = BOTS.find(([name]) => new RegExp("(^|[^A-Za-z0-9_-])" + name + "($|[^A-Za-z0-9_-])", "i").test(userAgent));
184
+ const botCandidates = attribution !== null ? ALL_BOTS : BOTS;
185
+ const bot = botCandidates.find(([name]) => new RegExp("(^|[^A-Za-z0-9_-])" + name + "($|[^A-Za-z0-9_-])", "i").test(userAgent));
186
+ const isProtocol = Boolean(bot && PROTOCOL_BOTS.some(([name]) => name === bot[0]));
187
+ const evidenceLevel = bot ? (isProtocol ? "protocol_claim" : "ua_claim") : (
188
+ attribution && attribution.claim_axis === "protocol_claim" ? "protocol_claim" : "unclassified"
189
+ );
144
190
  const body = {
145
191
  ...fields,
146
192
  provider: bot ? bot[1] : null,
147
193
  bot: bot ? bot[0] : null,
148
- evidence_level: bot ? "ua_claim" : "unclassified",
194
+ evidence_level: evidenceLevel,
149
195
  user_agent_digest: crypto.createHash("sha256").update(userAgent, "utf8").digest("hex"),
150
196
  };
197
+ if (attribution !== null) {
198
+ const claimAxis = attribution.claim_axis || evidenceLevel;
199
+ requireValue(claimAxis === evidenceLevel, "visit_claim_axis_invalid");
200
+ Object.assign(body, {
201
+ subject_namespace: attribution.subject_namespace,
202
+ subject_id: attribution.subject_id,
203
+ card_digest: attribution.card_digest,
204
+ claim_axis: claimAxis,
205
+ network_axis: attribution.network_axis,
206
+ identity_axis: attribution.identity_axis,
207
+ });
208
+ }
151
209
  return { ...body, digest: digest(body, canonicalJsonBuffer) };
152
210
  }
153
211
 
@@ -174,13 +232,25 @@ function createDiscoveryEvidenceApi(canonicalJsonBuffer) {
174
232
  function validateVisit(row, ownerHost) {
175
233
  requireValue(isRecord(row) && row.schema === VISIT_EVIDENCE_SCHEMA && row.host === ownerHost, "visit_invalid");
176
234
  verifyDigest(row, "visit_digest_mismatch");
177
- requireValue(hasFields(row, VISIT_FIELDS), "visit_invalid");
178
- const fields = visitFields(row, ownerHost);
235
+ const hasAttribution = CARD_ATTRIBUTION_FIELDS.some(key => Object.hasOwn(row, key));
236
+ requireValue(hasFields(row, hasAttribution ? [...VISIT_FIELDS, ...CARD_ATTRIBUTION_FIELDS] : VISIT_FIELDS), "visit_invalid");
237
+ const fields = visitFields(row, ownerHost, { allowHostedCard: hasAttribution });
179
238
  requireValue(row.path === fields.path && row.surface === fields.surface, "visit_invalid");
180
239
  requireValue(isSha256(row.user_agent_digest), "visit_invalid");
181
- const bot = BOTS.find(([name]) => name === row.bot);
240
+ const botTable = hasAttribution ? ALL_BOTS : BOTS;
241
+ const bot = botTable.find(([name]) => name === row.bot);
182
242
  requireValue(row.bot === null || bot !== undefined, "visit_invalid");
183
- requireValue(row.provider === (bot ? bot[1] : null) && row.evidence_level === (bot ? "ua_claim" : "unclassified"), "visit_invalid");
243
+ requireValue(row.provider === (bot ? bot[1] : null), "visit_invalid");
244
+ if (hasAttribution) {
245
+ const attribution = cardAttributionInput(row);
246
+ const isProtocol = Boolean(bot && PROTOCOL_BOTS.some(([name]) => name === bot[0]));
247
+ const expectedLevel = bot ? (isProtocol ? "protocol_claim" : "ua_claim") : (
248
+ attribution.claim_axis === "protocol_claim" ? "protocol_claim" : "unclassified"
249
+ );
250
+ requireValue(row.evidence_level === expectedLevel && row.claim_axis === expectedLevel, "visit_invalid");
251
+ } else {
252
+ requireValue(row.evidence_level === (bot ? "ua_claim" : "unclassified"), "visit_invalid");
253
+ }
184
254
  }
185
255
 
186
256
  function validateExposure(row, ownerHost) {
@@ -226,7 +296,7 @@ function createDiscoveryEvidenceApi(canonicalJsonBuffer) {
226
296
  for (const row of visitRows) {
227
297
  const count = row.request_count;
228
298
  const success = row.status >= 200 && row.status < 400;
229
- const cardSuccess = row.status >= 200 && row.status < 300 && row.surface === "agent_card";
299
+ const cardSuccess = row.status >= 200 && row.status < 300 && (row.surface === "agent_card" || row.surface === "hosted_card");
230
300
  successful += success ? count : 0;
231
301
  failed += success ? 0 : count;
232
302
  cardReads += cardSuccess ? count : 0;
@@ -276,7 +346,7 @@ function createDiscoveryEvidenceApi(canonicalJsonBuffer) {
276
346
  summaryCount(count);
277
347
  requireValue(field === "by_date" || count > 0, "summary_invalid");
278
348
  if (field === "by_path") requireValue(pathValue(key) === key, "summary_invalid");
279
- else if (field === "by_bot_claim") requireValue(BOTS.some(([name]) => name === key), "summary_invalid");
349
+ else if (field === "by_bot_claim") requireValue(ALL_BOTS.some(([name]) => name === key), "summary_invalid");
280
350
  else dateValue(key);
281
351
  }
282
352
  if (total !== null) requireValue(Object.values(values).reduce((sum, value) => sum + value, 0) === total, "summary_invalid");
package/js/discovery.d.ts CHANGED
@@ -89,6 +89,7 @@ export {
89
89
  resolveDocumentPath,
90
90
  signA2AAgentCard,
91
91
  signDescriptor,
92
+ summarizeCohortVisibility,
92
93
  summarizeDiscoveryEvidence,
93
94
  summarizeEngineObservations,
94
95
  unsignedDescriptor,
package/js/discovery.js CHANGED
@@ -16,6 +16,7 @@ module.exports = {
16
16
  EngineObservationError: impl.engineObservation.EngineObservationError,
17
17
  normalizeEngineObservation: impl.engineObservationApi.normalizeEngineObservation,
18
18
  summarizeEngineObservations: impl.engineObservationApi.summarizeEngineObservations,
19
+ summarizeCohortVisibility: impl.engineObservationApi.summarizeCohortVisibility,
19
20
  createSovereignCard: impl.createSovereignCard,
20
21
  createTwin3AgentCard: impl.createTwin3AgentCard,
21
22
  createAgentCard: impl.createAgentCard,
package/js/discovery.mjs CHANGED
@@ -13,6 +13,7 @@ export const ENGINE_READING_SCHEMA = api.ENGINE_READING_SCHEMA;
13
13
  export const EngineObservationError = api.EngineObservationError;
14
14
  export const normalizeEngineObservation = api.normalizeEngineObservation;
15
15
  export const summarizeEngineObservations = api.summarizeEngineObservations;
16
+ export const summarizeCohortVisibility = api.summarizeCohortVisibility;
16
17
  export const createSovereignCard = api.createSovereignCard;
17
18
  export const createTwin3AgentCard = api.createTwin3AgentCard;
18
19
  export const createAgentCard = api.createAgentCard;