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 +6 -4
- package/index.d.ts +73 -0
- package/js/comm/completion.js +157 -0
- package/js/comm/counterparty.js +14 -1
- package/js/comm.d.ts +6 -0
- package/js/comm.js +6 -0
- package/js/comm.mjs +5 -0
- package/js/discovery-evidence.js +81 -11
- package/js/discovery.d.ts +1 -0
- package/js/discovery.js +1 -0
- package/js/discovery.mjs +1 -0
- package/js/engine-observation.js +122 -1
- package/js/index.js +8 -0
- package/js/index.mjs +8 -0
- package/js/internal/comm.js +112 -1
- package/js/internal/shield.js +197 -1
- package/js/runtime-session.js +299 -0
- package/js/runtime.js +258 -0
- package/js/runtime.mjs +5 -0
- package/js/shield.d.ts +2 -0
- package/js/shield.js +2 -0
- package/js/shield.mjs +2 -0
- package/package.json +13 -1
- package/runtime.d.ts +141 -0
- package/twin3/comm/task-completion-command.schema.json +88 -0
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.
|
|
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.
|
|
127
|
-
pip install twin3-sdk==0.2.
|
|
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.
|
|
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 };
|
package/js/comm/counterparty.js
CHANGED
|
@@ -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;
|
package/js/discovery-evidence.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
-
|
|
178
|
-
|
|
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
|
|
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)
|
|
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(
|
|
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
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;
|