@iicp/web-node 0.2.4 → 0.2.5
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 +19 -0
- package/dist/build-manifest.json +3 -3
- package/dist/iicpConsumer.d.ts +0 -1
- package/dist/iicpConsumer.js +46 -2
- package/dist/runtimeIdentity.d.ts +8 -2
- package/dist/runtimeIdentity.js +65 -9
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,6 +5,11 @@ from the network *and* **serve** a model from a browser tab. ESM, built on the p
|
|
|
5
5
|
native `fetch` / `SubtleCrypto` / WebGPU (and runs in Node ≥18). The browser
|
|
6
6
|
client refuses keyless providers rather than silently sending plaintext.
|
|
7
7
|
|
|
8
|
+
IICP supplies intent resolution and provider eligibility/selection. MCP, A2A,
|
|
9
|
+
HTTP or another negotiated binding may then execute the selected task. See the
|
|
10
|
+
public [protocol positioning](https://github.com/RobLe3/IICP/blob/main/standards/IICP_PROTOCOL_POSITIONING.md)
|
|
11
|
+
and [adjacent-protocol comparison](https://github.com/RobLe3/IICP/blob/main/standards/PROTOCOL_COMPARISON_2026-08-15.md).
|
|
12
|
+
|
|
8
13
|
```
|
|
9
14
|
npm install @iicp/web-node
|
|
10
15
|
```
|
|
@@ -36,6 +41,20 @@ When the chosen node advertises an encryption key (`nodeCxKey(node)`), the paylo
|
|
|
36
41
|
no opt-out. A node that does not advertise `cx_public_key`/`public_key` is refused before
|
|
37
42
|
any network send so browser use cannot silently fall back to plaintext.
|
|
38
43
|
|
|
44
|
+
## Runtime self-description
|
|
45
|
+
|
|
46
|
+
Browser `chat()` calls add a small IICP runtime identity context by default. It
|
|
47
|
+
tells the selected model that it is being accessed through IICP, identifies the
|
|
48
|
+
active intent and browser package version, and includes a model or capability
|
|
49
|
+
fact only when it comes from the selected node's advertisement. It does not
|
|
50
|
+
expose endpoints, full node identities, candidate sets, scores or credentials,
|
|
51
|
+
and it is not a prompt-injection security boundary.
|
|
52
|
+
|
|
53
|
+
Set `runtime_identity: { mode: "disabled" }` to preserve the application
|
|
54
|
+
messages without the capsule, or use `mode: "required"` to refuse when a
|
|
55
|
+
supported instruction channel is unavailable. Non-chat operations and the
|
|
56
|
+
plaintext envelope helper remain unchanged.
|
|
57
|
+
|
|
39
58
|
## Serve — be a node from the browser
|
|
40
59
|
|
|
41
60
|
```ts
|
package/dist/build-manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "iicp.browser_build_provenance.v1",
|
|
3
3
|
"package": "@iicp/web-node",
|
|
4
|
-
"implementation_version": "0.2.
|
|
4
|
+
"implementation_version": "0.2.5",
|
|
5
5
|
"sdk_compatibility_version": "0.7.102",
|
|
6
|
-
"source_commit": "
|
|
7
|
-
"lockfile_sha256": "
|
|
6
|
+
"source_commit": "6f28256d0d67cf08beddadbc762dbd339786fcd8",
|
|
7
|
+
"lockfile_sha256": "1a398bdcc85e5a650a394aac26851a04c44bd3e6ec9c8c1c359d4d3015338a7e"
|
|
8
8
|
}
|
package/dist/iicpConsumer.d.ts
CHANGED
|
@@ -170,7 +170,6 @@ export declare function createRedactedRoutingReceipt(args: {
|
|
|
170
170
|
allowedRegions?: readonly string[] | null;
|
|
171
171
|
requiredManifestIdentityLevel?: RequiredManifestIdentityLevel | null;
|
|
172
172
|
}): RoutingReceipt;
|
|
173
|
-
/** Browser-native, consumer-only IICP client. */
|
|
174
173
|
export declare class IicpBrowserClient {
|
|
175
174
|
private readonly directory;
|
|
176
175
|
private readonly timeout;
|
package/dist/iicpConsumer.js
CHANGED
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
// Epic: #446 · Dev: #447 · Research: research/wasm/WASM-1-feasibility.md (#292).
|
|
18
18
|
import { encryptPayload } from "./cxConfidentiality.js";
|
|
19
19
|
import { verifyDispatchTicket } from "./dispatchTicket.js";
|
|
20
|
-
import { composeRuntimeIdentity } from "./runtimeIdentity.js";
|
|
20
|
+
import { composeRuntimeIdentity, withRuntimeFacts } from "./runtimeIdentity.js";
|
|
21
|
+
import { BROWSER_NODE_VERSION } from "./version.js";
|
|
21
22
|
const REFUSED_INTENT_RULES = [
|
|
22
23
|
{ category: "prohibited", rule_id: "eu-ai-act-social-scoring", label: "social scoring", fragments: ["social-scoring", "social_scoring", "social:scoring"] },
|
|
23
24
|
{ category: "prohibited", rule_id: "eu-ai-act-criminal-risk", label: "individual criminal risk prediction", fragments: ["criminal-risk", "criminal_risk", "criminal:risk", "predict-crime"] },
|
|
@@ -273,6 +274,41 @@ export function createRedactedRoutingReceipt(args) {
|
|
|
273
274
|
};
|
|
274
275
|
}
|
|
275
276
|
/** Browser-native, consumer-only IICP client. */
|
|
277
|
+
function selectedAdvertisedModel(node, requestedModel) {
|
|
278
|
+
if (requestedModel && node?.models?.includes(requestedModel))
|
|
279
|
+
return requestedModel;
|
|
280
|
+
return node?.models?.length === 1 ? node.models[0] : undefined;
|
|
281
|
+
}
|
|
282
|
+
function effectiveCapabilityLabels(node, intent, model) {
|
|
283
|
+
const raw = node?.capabilities;
|
|
284
|
+
if (!Array.isArray(raw))
|
|
285
|
+
return [];
|
|
286
|
+
const variants = raw.filter((candidate) => Boolean(candidate) && typeof candidate === "object" && candidate.intent === intent);
|
|
287
|
+
const exact = model
|
|
288
|
+
? variants.filter((candidate) => Array.isArray(candidate.models) && candidate.models.includes(model))
|
|
289
|
+
: variants;
|
|
290
|
+
const selected = exact.length === 1 ? exact[0] : variants.length === 1 ? variants[0] : undefined;
|
|
291
|
+
if (!selected)
|
|
292
|
+
return [];
|
|
293
|
+
const labels = [];
|
|
294
|
+
for (const value of Array.isArray(selected.input_modalities) ? selected.input_modalities : []) {
|
|
295
|
+
if (typeof value === "string")
|
|
296
|
+
labels.push(`input_modality:${value}`);
|
|
297
|
+
}
|
|
298
|
+
for (const value of Array.isArray(selected.output_modalities) ? selected.output_modalities : []) {
|
|
299
|
+
if (typeof value === "string")
|
|
300
|
+
labels.push(`output_modality:${value}`);
|
|
301
|
+
}
|
|
302
|
+
for (const value of Array.isArray(selected.features) ? selected.features : []) {
|
|
303
|
+
if (typeof value === "string")
|
|
304
|
+
labels.push(value);
|
|
305
|
+
}
|
|
306
|
+
for (const value of Array.isArray(selected.execution_capabilities) ? selected.execution_capabilities : []) {
|
|
307
|
+
if (typeof value === "string")
|
|
308
|
+
labels.push(`execution:${value}`);
|
|
309
|
+
}
|
|
310
|
+
return [...new Set(labels)].sort();
|
|
311
|
+
}
|
|
276
312
|
export class IicpBrowserClient {
|
|
277
313
|
directory;
|
|
278
314
|
timeout;
|
|
@@ -444,7 +480,15 @@ export class IicpBrowserClient {
|
|
|
444
480
|
async chatWithReceipt(messages, opts) {
|
|
445
481
|
const intent = opts.intent ?? "urn:iicp:intent:llm:chat:v1";
|
|
446
482
|
validateIntent(intent);
|
|
447
|
-
|
|
483
|
+
const selectedModel = selectedAdvertisedModel(opts.node, opts.model);
|
|
484
|
+
messages = composeRuntimeIdentity(messages, intent, withRuntimeFacts(opts.runtime_identity, {
|
|
485
|
+
client_name: "@iicp/web-node",
|
|
486
|
+
client_version: BROWSER_NODE_VERSION,
|
|
487
|
+
connection_mode: "routed",
|
|
488
|
+
selected_model: selectedModel,
|
|
489
|
+
effective_capabilities: effectiveCapabilityLabels(opts.node, intent, selectedModel),
|
|
490
|
+
selection_reason: "matched_intent_and_constraints",
|
|
491
|
+
}));
|
|
448
492
|
const taskId = globalThis.crypto?.randomUUID?.() ?? `task-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
449
493
|
const payload = { messages, model: opts.model };
|
|
450
494
|
// IICP-CX S.16: encryption is MANDATORY (privacy-first #360) — no opt-out.
|
|
@@ -3,17 +3,23 @@ export declare const RUNTIME_IDENTITY_PROFILE_ID = "urn:iicp:profile:runtime-ide
|
|
|
3
3
|
export declare const RUNTIME_IDENTITY_MARKER = "IICP-RUNTIME-CONTEXT/1";
|
|
4
4
|
export declare const RUNTIME_IDENTITY_CHAT_INTENT = "urn:iicp:intent:llm:chat:v1";
|
|
5
5
|
export declare const RUNTIME_IDENTITY_MAX_BYTES = 2048;
|
|
6
|
-
export type RuntimeIdentityMode = "disabled" | "explicit" | "required";
|
|
6
|
+
export type RuntimeIdentityMode = "auto" | "disabled" | "explicit" | "required";
|
|
7
7
|
export type RuntimeIdentityInstructionChannel = "system" | "unsupported";
|
|
8
|
+
export type RuntimeIdentityConnectionMode = "routed" | "local_browser";
|
|
9
|
+
export type RuntimeIdentitySelectionReason = "matched_intent_and_constraints" | "explicit_model_match" | "fallback_after_unavailable_candidate" | "intentional_exploration" | "local_browser_execution";
|
|
8
10
|
export interface RuntimeIdentityOptions {
|
|
9
11
|
mode?: RuntimeIdentityMode;
|
|
10
12
|
instruction_channel?: RuntimeIdentityInstructionChannel;
|
|
11
13
|
selected_model?: string;
|
|
12
14
|
effective_capabilities?: string[];
|
|
13
|
-
selection_reason?:
|
|
15
|
+
selection_reason?: RuntimeIdentitySelectionReason;
|
|
16
|
+
client_name?: string;
|
|
17
|
+
client_version?: string;
|
|
18
|
+
connection_mode?: RuntimeIdentityConnectionMode;
|
|
14
19
|
}
|
|
15
20
|
export declare class RuntimeIdentityContextUnsupported extends Error {
|
|
16
21
|
constructor();
|
|
17
22
|
}
|
|
23
|
+
export declare function withRuntimeFacts(options: RuntimeIdentityOptions | undefined, facts: Required<Pick<RuntimeIdentityOptions, "client_name" | "client_version" | "connection_mode" | "selection_reason">> & Pick<RuntimeIdentityOptions, "selected_model" | "effective_capabilities">): RuntimeIdentityOptions;
|
|
18
24
|
export declare function renderRuntimeIdentity(intent: string, options: RuntimeIdentityOptions): string;
|
|
19
25
|
export declare function composeRuntimeIdentity(messages: readonly ChatMessage[], intent: string, options?: RuntimeIdentityOptions): ChatMessage[];
|
package/dist/runtimeIdentity.js
CHANGED
|
@@ -2,6 +2,8 @@ export const RUNTIME_IDENTITY_PROFILE_ID = "urn:iicp:profile:runtime-identity-co
|
|
|
2
2
|
export const RUNTIME_IDENTITY_MARKER = "IICP-RUNTIME-CONTEXT/1";
|
|
3
3
|
export const RUNTIME_IDENTITY_CHAT_INTENT = "urn:iicp:intent:llm:chat:v1";
|
|
4
4
|
export const RUNTIME_IDENTITY_MAX_BYTES = 2048;
|
|
5
|
+
const MAX_FACT_BYTES = 160;
|
|
6
|
+
const MAX_CAPABILITIES = 32;
|
|
5
7
|
const BASE_CAPSULE = "This request reached you through IICP, the Intent-based Inter-agent Communication Protocol. IICP discovers eligible services and routes requests. You are the selected model or service, not IICP. When asked about this connection, use only supplied runtime facts; do not guess missing facts.";
|
|
6
8
|
export class RuntimeIdentityContextUnsupported extends Error {
|
|
7
9
|
constructor() {
|
|
@@ -9,15 +11,59 @@ export class RuntimeIdentityContextUnsupported extends Error {
|
|
|
9
11
|
this.name = "RuntimeIdentityContextUnsupported";
|
|
10
12
|
}
|
|
11
13
|
}
|
|
14
|
+
const selectionText = {
|
|
15
|
+
matched_intent_and_constraints: "This service matched the requested intent and constraints.",
|
|
16
|
+
explicit_model_match: "This service matched the requested model and constraints.",
|
|
17
|
+
fallback_after_unavailable_candidate: "This service was selected after an earlier candidate was unavailable.",
|
|
18
|
+
intentional_exploration: "This service was selected for an intentional routing exploration.",
|
|
19
|
+
local_browser_execution: "This model is running locally in the browser.",
|
|
20
|
+
};
|
|
21
|
+
export function withRuntimeFacts(options, facts) {
|
|
22
|
+
return {
|
|
23
|
+
...(options ?? {}),
|
|
24
|
+
...facts,
|
|
25
|
+
selected_model: facts.selected_model,
|
|
26
|
+
effective_capabilities: [...(facts.effective_capabilities ?? [])],
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function boundedFact(value, name) {
|
|
30
|
+
if (!value || /[\u0000-\u001f\u007f]/u.test(value)) {
|
|
31
|
+
throw new Error(`runtime identity ${name} contains control characters`);
|
|
32
|
+
}
|
|
33
|
+
if (new TextEncoder().encode(value).byteLength > MAX_FACT_BYTES) {
|
|
34
|
+
throw new Error(`runtime identity ${name} exceeds the bounded fact limit`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
12
38
|
export function renderRuntimeIdentity(intent, options) {
|
|
13
|
-
const lines = [`[${RUNTIME_IDENTITY_MARKER}]`, BASE_CAPSULE, "Runtime facts:", `- intent: ${intent}`];
|
|
39
|
+
const lines = [`[${RUNTIME_IDENTITY_MARKER}]`, BASE_CAPSULE, "Runtime facts:", `- intent: ${boundedFact(intent, "intent")}`];
|
|
40
|
+
if (options.client_name || options.client_version) {
|
|
41
|
+
if (!options.client_name || !options.client_version)
|
|
42
|
+
throw new Error("runtime identity client name and version must be supplied together");
|
|
43
|
+
lines.push(`- client: ${boundedFact(options.client_name, "client name")} ${boundedFact(options.client_version, "client version")}`);
|
|
44
|
+
}
|
|
45
|
+
if (options.connection_mode === "routed") {
|
|
46
|
+
lines.push("- connection: routed through IICP to an eligible provider.");
|
|
47
|
+
}
|
|
48
|
+
else if (options.connection_mode === "local_browser") {
|
|
49
|
+
lines.push("- connection: This model is running locally in the browser; no remote IICP provider was selected.");
|
|
50
|
+
}
|
|
51
|
+
else if (options.connection_mode !== undefined) {
|
|
52
|
+
throw new Error("runtime identity connection mode is unsupported");
|
|
53
|
+
}
|
|
14
54
|
if (options.selected_model)
|
|
15
|
-
lines.push(`- selected model
|
|
55
|
+
lines.push(`- selected model: ${boundedFact(options.selected_model, "selected model")}`);
|
|
56
|
+
if ((options.effective_capabilities?.length ?? 0) > MAX_CAPABILITIES) {
|
|
57
|
+
throw new Error("runtime identity effective capabilities exceed the bounded count");
|
|
58
|
+
}
|
|
16
59
|
if (options.effective_capabilities?.length) {
|
|
17
|
-
lines.push(`- effective capabilities: ${options.effective_capabilities.join(", ")}`);
|
|
60
|
+
lines.push(`- effective capabilities: ${options.effective_capabilities.map((value) => boundedFact(value, "effective capability")).join(", ")}`);
|
|
18
61
|
}
|
|
19
|
-
if (options.selection_reason
|
|
20
|
-
|
|
62
|
+
if (options.selection_reason) {
|
|
63
|
+
const selection = selectionText[options.selection_reason];
|
|
64
|
+
if (!selection)
|
|
65
|
+
throw new Error("runtime identity selection reason is unsupported");
|
|
66
|
+
lines.push(`- selection: ${selection}`);
|
|
21
67
|
}
|
|
22
68
|
const rendered = lines.join("\n");
|
|
23
69
|
if (new TextEncoder().encode(rendered).byteLength > RUNTIME_IDENTITY_MAX_BYTES) {
|
|
@@ -27,10 +73,20 @@ export function renderRuntimeIdentity(intent, options) {
|
|
|
27
73
|
}
|
|
28
74
|
export function composeRuntimeIdentity(messages, intent, options) {
|
|
29
75
|
const original = [...messages];
|
|
30
|
-
const
|
|
31
|
-
|
|
76
|
+
const resolved = options ?? {};
|
|
77
|
+
const mode = resolved.mode ?? "auto";
|
|
78
|
+
if (intent !== RUNTIME_IDENTITY_CHAT_INTENT)
|
|
32
79
|
return original;
|
|
33
|
-
if (
|
|
80
|
+
if (!["auto", "disabled", "explicit", "required"].includes(mode)) {
|
|
81
|
+
throw new Error("runtime identity mode is unsupported");
|
|
82
|
+
}
|
|
83
|
+
if (mode === "disabled")
|
|
84
|
+
return original;
|
|
85
|
+
if (resolved.instruction_channel !== undefined
|
|
86
|
+
&& !["system", "unsupported"].includes(resolved.instruction_channel)) {
|
|
87
|
+
throw new Error("runtime identity instruction channel is unsupported");
|
|
88
|
+
}
|
|
89
|
+
if (resolved.instruction_channel === "unsupported") {
|
|
34
90
|
if (mode === "required")
|
|
35
91
|
throw new RuntimeIdentityContextUnsupported();
|
|
36
92
|
return original;
|
|
@@ -43,7 +99,7 @@ export function composeRuntimeIdentity(messages, intent, options) {
|
|
|
43
99
|
insertion += 1;
|
|
44
100
|
return [
|
|
45
101
|
...original.slice(0, insertion),
|
|
46
|
-
{ role: "system", content: renderRuntimeIdentity(intent,
|
|
102
|
+
{ role: "system", content: renderRuntimeIdentity(intent, resolved) },
|
|
47
103
|
...original.slice(insertion),
|
|
48
104
|
];
|
|
49
105
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const BROWSER_NODE_VERSION = "0.2.
|
|
1
|
+
export declare const BROWSER_NODE_VERSION = "0.2.5";
|
|
2
2
|
export declare const BROWSER_NODE_SDK_COMPATIBILITY_VERSION = "0.7.102";
|
|
3
3
|
/** Backward-compatible registration value for directories that know only sdk_version. */
|
|
4
4
|
export declare const BROWSER_NODE_SDK_VERSION = "0.7.102";
|
package/dist/version.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Browser implementation and IICP SDK-compatibility versions are separate axes.
|
|
2
2
|
// Keep package.json synchronized with BROWSER_NODE_VERSION; the quality gate
|
|
3
3
|
// rejects drift. SDK compatibility describes the registration contract only.
|
|
4
|
-
export const BROWSER_NODE_VERSION = "0.2.
|
|
4
|
+
export const BROWSER_NODE_VERSION = "0.2.5";
|
|
5
5
|
export const BROWSER_NODE_SDK_COMPATIBILITY_VERSION = "0.7.102";
|
|
6
6
|
/** Backward-compatible registration value for directories that know only sdk_version. */
|
|
7
7
|
export const BROWSER_NODE_SDK_VERSION = BROWSER_NODE_SDK_COMPATIBILITY_VERSION;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@iicp/web-node",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
4
4
|
"description": "Browser-native IICP node (consume + serve): discovery-mesh client with mandatory E2E encryption (IICP-CX) + WebLLM provider. Zero-config, ESM.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|