@kognai/tool-router 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +47 -2
- package/dist/adapters/awesome-list.d.ts +10 -0
- package/dist/adapters/awesome-list.js +68 -0
- package/dist/adapters/mcp-registry.d.ts +12 -0
- package/dist/adapters/mcp-registry.js +51 -0
- package/dist/adapters/mcp-tools.d.ts +49 -0
- package/dist/adapters/mcp-tools.js +60 -0
- package/dist/adapters/openapi.d.ts +25 -0
- package/dist/adapters/openapi.js +107 -0
- package/dist/adapters/pay-skills.d.ts +13 -0
- package/dist/adapters/pay-skills.js +119 -0
- package/dist/adapters/raw.d.ts +82 -0
- package/dist/adapters/raw.js +5 -0
- package/dist/adapters/x402-discovery.d.ts +6 -0
- package/dist/adapters/x402-discovery.js +100 -0
- package/dist/catalog/file-store.d.ts +27 -0
- package/dist/catalog/file-store.js +102 -0
- package/dist/catalog/ingest.d.ts +53 -0
- package/dist/catalog/ingest.js +276 -0
- package/dist/catalog/lifecycle.d.ts +24 -0
- package/dist/catalog/lifecycle.js +51 -0
- package/dist/catalog/memory-store.d.ts +49 -0
- package/dist/catalog/memory-store.js +92 -0
- package/dist/catalog/risk.d.ts +22 -0
- package/dist/catalog/risk.js +73 -0
- package/dist/catalog/sanitise.d.ts +17 -0
- package/dist/catalog/sanitise.js +109 -0
- package/dist/classify/classifier.d.ts +12 -0
- package/dist/classify/classifier.js +52 -0
- package/dist/classify/decompose.d.ts +10 -0
- package/dist/classify/decompose.js +107 -0
- package/dist/classify/rules.d.ts +46 -0
- package/dist/classify/rules.js +249 -0
- package/dist/classify/tagger.d.ts +5 -0
- package/dist/classify/tagger.js +29 -0
- package/dist/classify/taxonomy.d.ts +31 -0
- package/dist/classify/taxonomy.js +221 -0
- package/dist/classify/text.d.ts +9 -0
- package/dist/classify/text.js +38 -0
- package/dist/eligibility/engine.d.ts +11 -0
- package/dist/eligibility/engine.js +19 -0
- package/dist/eligibility/rules.d.ts +33 -0
- package/dist/eligibility/rules.js +121 -0
- package/dist/flags.d.ts +33 -0
- package/dist/flags.js +34 -0
- package/dist/hash.d.ts +8 -0
- package/dist/hash.js +30 -0
- package/dist/ids.d.ts +17 -0
- package/dist/ids.js +54 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +95 -0
- package/dist/money.d.ts +10 -0
- package/dist/money.js +41 -0
- package/dist/rank/confidence.d.ts +15 -0
- package/dist/rank/confidence.js +48 -0
- package/dist/rank/ranker.d.ts +34 -0
- package/dist/rank/ranker.js +81 -0
- package/dist/rank/weights.d.ts +40 -0
- package/dist/rank/weights.js +46 -0
- package/dist/retrieve/bm25.d.ts +21 -0
- package/dist/retrieve/bm25.js +59 -0
- package/dist/retrieve/capability.d.ts +1 -0
- package/dist/retrieve/capability.js +13 -0
- package/dist/retrieve/hash-embed.d.ts +4 -0
- package/dist/retrieve/hash-embed.js +41 -0
- package/dist/retrieve/hybrid.d.ts +74 -0
- package/dist/retrieve/hybrid.js +125 -0
- package/dist/retrieve/schema-fit.d.ts +9 -0
- package/dist/retrieve/schema-fit.js +38 -0
- package/dist/retrieve/tokenise.d.ts +2 -0
- package/dist/retrieve/tokenise.js +51 -0
- package/dist/retrieve/vector.d.ts +18 -0
- package/dist/retrieve/vector.js +47 -0
- package/dist/router.d.ts +37 -0
- package/dist/router.js +114 -0
- package/dist/schema/tool.schema.json +117 -0
- package/dist/schema/validate.d.ts +9 -0
- package/dist/schema/validate.js +92 -0
- package/dist/shortlist/generate.d.ts +21 -0
- package/dist/shortlist/generate.js +75 -0
- package/dist/types/ports.d.ts +71 -0
- package/dist/types/ports.js +2 -0
- package/dist/types/route.d.ts +121 -0
- package/dist/types/route.js +5 -0
- package/dist/types/tool.d.ts +123 -0
- package/dist/types/tool.js +17 -0
- package/package.json +39 -4
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The adapter contract: what every source adapter emits before ingestion. Adapters are pure functions over pinned
|
|
3
|
+
* input (a parsed file); none of them fetches, installs or runs anything.
|
|
4
|
+
*/
|
|
5
|
+
import type { BusinessTier, JSONSchema, Lifecycle, Network, Origin, Pricing, PrivacyClass, SideEffect, ToolRecord } from '../types/tool';
|
|
6
|
+
export interface RawOp {
|
|
7
|
+
op: string;
|
|
8
|
+
summary: string;
|
|
9
|
+
input_schema?: JSONSchema | null;
|
|
10
|
+
output_schema?: JSONSchema | null;
|
|
11
|
+
/** null or absent = unknown, which ingestion treats as network + data modification (never safe). */
|
|
12
|
+
side_effects?: SideEffect[] | null;
|
|
13
|
+
scopes?: string[];
|
|
14
|
+
idempotent?: boolean | null;
|
|
15
|
+
capabilities?: string[];
|
|
16
|
+
method?: string;
|
|
17
|
+
path?: string;
|
|
18
|
+
}
|
|
19
|
+
/** Where a descriptor came from; every adapter receives this from the caller. */
|
|
20
|
+
export interface ProvenanceIn {
|
|
21
|
+
source: string;
|
|
22
|
+
retrieved_at: string;
|
|
23
|
+
licence: string | null;
|
|
24
|
+
snapshot_sha256: string;
|
|
25
|
+
}
|
|
26
|
+
export interface RawToolDescriptor {
|
|
27
|
+
source: string;
|
|
28
|
+
source_ref: string;
|
|
29
|
+
origin: Origin;
|
|
30
|
+
/** namespace/name for the tool id (ADR-004), before normalisation. */
|
|
31
|
+
path: string;
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
provider: {
|
|
35
|
+
name: string;
|
|
36
|
+
url?: string;
|
|
37
|
+
kognai_owned?: boolean;
|
|
38
|
+
};
|
|
39
|
+
category?: string;
|
|
40
|
+
tags?: string[];
|
|
41
|
+
operations: RawOp[];
|
|
42
|
+
auth?: ToolRecord['auth'];
|
|
43
|
+
pricing?: Pricing;
|
|
44
|
+
pricing_text?: string;
|
|
45
|
+
licence?: string | null;
|
|
46
|
+
endpoint?: string;
|
|
47
|
+
package?: {
|
|
48
|
+
registry: string;
|
|
49
|
+
identifier: string;
|
|
50
|
+
version?: string;
|
|
51
|
+
};
|
|
52
|
+
/** 'package' = code to install (never run by KTR), 'remote' = a hosted endpoint, 'native' = Kognai code in repo. */
|
|
53
|
+
implementation?: 'package' | 'remote' | 'native' | 'http' | 'metadata';
|
|
54
|
+
version?: string;
|
|
55
|
+
privacy_class?: PrivacyClass;
|
|
56
|
+
business_tier?: BusinessTier | null;
|
|
57
|
+
regions?: string[];
|
|
58
|
+
networks?: Network[];
|
|
59
|
+
product_id?: string;
|
|
60
|
+
/** The provider serves a valid AIAX manifest (checked by the caller; only true is carried into the record). */
|
|
61
|
+
aiax_ready?: boolean;
|
|
62
|
+
aliases?: string[];
|
|
63
|
+
skill_ids?: string[];
|
|
64
|
+
lifecycle?: Lifecycle;
|
|
65
|
+
dependencies?: string[];
|
|
66
|
+
/** A digest of the implementation when the source knows it (package version, file content of native code). */
|
|
67
|
+
implementation_hash?: string;
|
|
68
|
+
/** Tags the tool as a Kognai release path that keeps the Founder's approval (E4 publish exception). */
|
|
69
|
+
founder_approval?: boolean;
|
|
70
|
+
provenance: ProvenanceIn & {
|
|
71
|
+
adapter: string;
|
|
72
|
+
adapter_version: string;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export declare const prov: (p: ProvenanceIn, adapter: string, adapter_version: string) => {
|
|
76
|
+
adapter: string;
|
|
77
|
+
adapter_version: string;
|
|
78
|
+
source: string;
|
|
79
|
+
retrieved_at: string;
|
|
80
|
+
licence: string | null;
|
|
81
|
+
snapshot_sha256: string;
|
|
82
|
+
};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Network } from '../types/tool';
|
|
2
|
+
import type { ProvenanceIn, RawToolDescriptor } from './raw';
|
|
3
|
+
export declare const ADAPTER = "x402-discovery";
|
|
4
|
+
export declare const ADAPTER_VERSION = "1.0.0";
|
|
5
|
+
export declare const networkOf: (n: unknown) => Network | null;
|
|
6
|
+
export declare function fromX402Resources(items: unknown, p: ProvenanceIn, sourceName?: string): RawToolDescriptor[];
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.networkOf = exports.ADAPTER_VERSION = exports.ADAPTER = void 0;
|
|
4
|
+
exports.fromX402Resources = fromX402Resources;
|
|
5
|
+
/**
|
|
6
|
+
* x402 discovery catalogs: PayAI (facilitator.payai.network/discovery/resources) and the Coinbase CDP Bazaar
|
|
7
|
+
* (api.cdp.coinbase.com/platform/v2/x402/discovery/resources). Accepts both the raw discovery items and the
|
|
8
|
+
* normalised resources Golden Kog keeps in its daily catalog (scripts/labor/golden-kog/intel.ts). The field mapping
|
|
9
|
+
* follows Golden Kog's (resource url, accepts[] offers in micro units, bazaar input), re-implemented here: the package
|
|
10
|
+
* imports nothing from scripts/. Payees are not copied into the catalog: an agent never chooses where money goes.
|
|
11
|
+
*/
|
|
12
|
+
const node_crypto_1 = require("node:crypto");
|
|
13
|
+
const raw_1 = require("./raw");
|
|
14
|
+
exports.ADAPTER = 'x402-discovery';
|
|
15
|
+
exports.ADAPTER_VERSION = '1.0.0';
|
|
16
|
+
const CAIP = {
|
|
17
|
+
'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp': 'solana',
|
|
18
|
+
'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1': 'solana-devnet',
|
|
19
|
+
'eip155:8453': 'base',
|
|
20
|
+
'eip155:84532': 'base-sepolia',
|
|
21
|
+
solana: 'solana', base: 'base', 'base-sepolia': 'base-sepolia', 'solana-devnet': 'solana-devnet',
|
|
22
|
+
};
|
|
23
|
+
const networkOf = (n) => CAIP[String(n || '')] || null;
|
|
24
|
+
exports.networkOf = networkOf;
|
|
25
|
+
function schemaFromInput(input) {
|
|
26
|
+
if (!input || typeof input !== 'object')
|
|
27
|
+
return null;
|
|
28
|
+
const properties = {};
|
|
29
|
+
const fromShape = (o) => {
|
|
30
|
+
if (!o || typeof o !== 'object')
|
|
31
|
+
return;
|
|
32
|
+
const props = o.properties && typeof o.properties === 'object' ? o.properties : o;
|
|
33
|
+
for (const [k, v] of Object.entries(props)) {
|
|
34
|
+
if (k === 'type' || k === 'required' || k === 'properties')
|
|
35
|
+
continue;
|
|
36
|
+
properties[k] = v && typeof v === 'object' && typeof v.type === 'string' ? { type: v.type, ...(v.description ? { description: String(v.description).slice(0, 200) } : {}) } : { type: typeof v === 'number' ? 'number' : 'string' };
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
fromShape(input.queryParams);
|
|
40
|
+
fromShape(input.body);
|
|
41
|
+
const required = Array.isArray(input.required) ? input.required.map(String) : [];
|
|
42
|
+
if (!Object.keys(properties).length && !required.length)
|
|
43
|
+
return null;
|
|
44
|
+
return { type: 'object', properties, ...(required.length ? { required } : {}) };
|
|
45
|
+
}
|
|
46
|
+
/** One item, raw (resource string, accepts[]) or Golden Kog normalised (url, offers[]). */
|
|
47
|
+
function norm(it) {
|
|
48
|
+
const url = String(typeof it?.resource === 'string' ? it.resource : it?.url || it?.resource?.url || '');
|
|
49
|
+
if (!/^https:\/\//i.test(url))
|
|
50
|
+
return null;
|
|
51
|
+
const offers = Array.isArray(it.accepts) ? it.accepts : Array.isArray(it.offers) ? it.offers : [];
|
|
52
|
+
const amounts = offers
|
|
53
|
+
.map((o) => ({ micro: String(o?.amount ?? o?.maxAmountRequired ?? ''), network: (0, exports.networkOf)(o?.network) }))
|
|
54
|
+
.filter((a) => /^\d{1,15}$/.test(a.micro));
|
|
55
|
+
const baseInput = it?.input || it?.extensions?.bazaar?.info?.input || it?.inputSchema || it?.accepts?.[0]?.outputSchema?.input || null;
|
|
56
|
+
const req = baseInput?.required
|
|
57
|
+
|| it?.extensions?.bazaar?.schema?.properties?.input?.properties?.body?.required
|
|
58
|
+
|| it?.extensions?.bazaar?.schema?.properties?.input?.properties?.queryParams?.required || [];
|
|
59
|
+
const input = baseInput ? { ...baseInput, required: Array.isArray(req) ? req.map(String) : [] } : null;
|
|
60
|
+
const method = String(baseInput?.method || it.method || 'GET').toUpperCase();
|
|
61
|
+
const description = String(it.description || it.metadata?.description || offers.map((o) => o?.description).filter(Boolean)[0] || '');
|
|
62
|
+
const tags = [...(Array.isArray(it.topics) ? it.topics : []), ...(Array.isArray(it.tags) ? it.tags : [])].map(String).slice(0, 10);
|
|
63
|
+
return { url, method, description, amounts, input, tags, source: String(it.source || '') };
|
|
64
|
+
}
|
|
65
|
+
function fromX402Resources(items, p, sourceName = 'x402-discovery') {
|
|
66
|
+
const list = Array.isArray(items) ? items : Array.isArray(items?.resources) ? items.resources : Array.isArray(items?.items) ? items.items : [];
|
|
67
|
+
const out = [];
|
|
68
|
+
const used = new Set();
|
|
69
|
+
for (const it of list) {
|
|
70
|
+
const r = norm(it);
|
|
71
|
+
if (!r)
|
|
72
|
+
continue;
|
|
73
|
+
const u = new URL(r.url);
|
|
74
|
+
const host = u.hostname.toLowerCase().replace(/^www\./, '');
|
|
75
|
+
let name = (u.pathname.replace(/^\/+|\/+$/g, '').replace(/\//g, '-') || 'root') + (r.method !== 'GET' ? `-${r.method.toLowerCase()}` : '');
|
|
76
|
+
// long resource paths (token addresses in the path) keep a readable head and a digest of the full resource
|
|
77
|
+
if (name.length > 48)
|
|
78
|
+
name = `${name.slice(0, 39).replace(/[-.]+$/, '')}-${(0, node_crypto_1.createHash)('sha256').update(`${r.method} ${r.url}`).digest('hex').slice(0, 8)}`;
|
|
79
|
+
let path = `${host}/${name}`;
|
|
80
|
+
for (let i = 2; used.has(path.toLowerCase()); i++)
|
|
81
|
+
path = `${host}/${name}-${i}`;
|
|
82
|
+
used.add(path.toLowerCase());
|
|
83
|
+
const networks = [...new Set(r.amounts.map((a) => a.network).filter(Boolean))];
|
|
84
|
+
const micros = r.amounts.map((a) => BigInt(a.micro));
|
|
85
|
+
const pricing = micros.length
|
|
86
|
+
? { model: 'per_call', price: { amount_micro: micros.reduce((a, b) => (b < a ? b : a)).toString(), currency: 'USDC', ...(networks[0] ? { network: networks[0] } : {}) }, unit: 'request', source: 'provider' }
|
|
87
|
+
: { model: 'unknown', source: 'unknown' };
|
|
88
|
+
const side = r.method === 'GET' || r.method === 'HEAD' ? ['read'] : null;
|
|
89
|
+
out.push({
|
|
90
|
+
source: sourceName, source_ref: `${r.method} ${r.url}`, origin: 'x402', path,
|
|
91
|
+
name: `${host} ${u.pathname}`.slice(0, 120), description: r.description,
|
|
92
|
+
provider: { name: host, url: `${u.protocol}//${u.host}`, kognai_owned: false },
|
|
93
|
+
category: 'x402-service', tags: ['x402', ...(r.source ? [r.source] : []), ...r.tags],
|
|
94
|
+
operations: [{ op: 'call', method: r.method, summary: r.description || `${r.method} ${u.pathname}`, input_schema: schemaFromInput(r.input), output_schema: null, side_effects: side }],
|
|
95
|
+
auth: { kind: 'x402' }, pricing, licence: p.licence, endpoint: r.url, implementation: 'http',
|
|
96
|
+
networks, lifecycle: 'active', provenance: (0, raw_1.prov)(p, exports.ADAPTER, exports.ADAPTER_VERSION),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return out;
|
|
100
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TrustEvent } from '../types/ports';
|
|
2
|
+
import type { ToolRecord, TrustState } from '../types/tool';
|
|
3
|
+
import { MemoryCatalogStore } from './memory-store';
|
|
4
|
+
export declare class FileCatalogStore extends MemoryCatalogStore {
|
|
5
|
+
readonly dir: string;
|
|
6
|
+
private loading;
|
|
7
|
+
constructor(dir: string);
|
|
8
|
+
private flush;
|
|
9
|
+
upsert(rows: ToolRecord[]): {
|
|
10
|
+
added: number;
|
|
11
|
+
versioned: number;
|
|
12
|
+
unchanged: number;
|
|
13
|
+
};
|
|
14
|
+
setTrust(id: string, version: string, to: TrustState, by: string, why: string): void;
|
|
15
|
+
recordEvent(e: TrustEvent): void;
|
|
16
|
+
writeRaw(sha: string, text: string): void;
|
|
17
|
+
vectors(model: string): {
|
|
18
|
+
dims: number;
|
|
19
|
+
ids: string[];
|
|
20
|
+
data: Float32Array;
|
|
21
|
+
} | null;
|
|
22
|
+
writeVectors(model: string, v: {
|
|
23
|
+
dims: number;
|
|
24
|
+
ids: string[];
|
|
25
|
+
data: Float32Array;
|
|
26
|
+
}, builtAt: string): void;
|
|
27
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FileCatalogStore = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* FileCatalogStore (ADR-003): workspace/ktr/catalog/
|
|
6
|
+
* tools.jsonl one row per tool version, rewritten by atomic rename
|
|
7
|
+
* index.json snapshot id + index entries (id, version, schema hash, trust, lifecycle, latest)
|
|
8
|
+
* events.jsonl trust events, append only
|
|
9
|
+
* raw/<sha>.txt raw provider descriptions, by hash, never shown to a model
|
|
10
|
+
* vectors.<model>.f32 float32 little endian, row major; vectors.<model>.json { model, dims, ids, built_at }
|
|
11
|
+
*/
|
|
12
|
+
const node_fs_1 = require("node:fs");
|
|
13
|
+
const node_path_1 = require("node:path");
|
|
14
|
+
const memory_store_1 = require("./memory-store");
|
|
15
|
+
const MODEL_RE = /^[a-z0-9.:_-]{1,60}$/i;
|
|
16
|
+
function atomicWrite(path, data) {
|
|
17
|
+
const tmp = `${path}.tmp-${process.pid}`;
|
|
18
|
+
(0, node_fs_1.writeFileSync)(tmp, data);
|
|
19
|
+
(0, node_fs_1.renameSync)(tmp, path);
|
|
20
|
+
}
|
|
21
|
+
class FileCatalogStore extends memory_store_1.MemoryCatalogStore {
|
|
22
|
+
dir;
|
|
23
|
+
loading = false;
|
|
24
|
+
constructor(dir) {
|
|
25
|
+
super();
|
|
26
|
+
this.dir = dir;
|
|
27
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
28
|
+
this.loading = true;
|
|
29
|
+
const tools = (0, node_path_1.join)(dir, 'tools.jsonl');
|
|
30
|
+
const index = (0, node_path_1.join)(dir, 'index.json');
|
|
31
|
+
if ((0, node_fs_1.existsSync)(tools)) {
|
|
32
|
+
const rows = (0, node_fs_1.readFileSync)(tools, 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
33
|
+
// restore ingestion order of latest versions from the index
|
|
34
|
+
const latest = (0, node_fs_1.existsSync)(index) ? new Map((JSON.parse((0, node_fs_1.readFileSync)(index, 'utf8')).entries || []).filter((e) => e.latest).map((e) => [e.id, e.version])) : new Map();
|
|
35
|
+
for (const r of rows)
|
|
36
|
+
this.rows.set(this.key(r.id, r.version), r);
|
|
37
|
+
for (const r of rows)
|
|
38
|
+
if (!latest.size || latest.get(r.id) === r.version)
|
|
39
|
+
this.latestVersion.set(r.id, r.version);
|
|
40
|
+
}
|
|
41
|
+
const ev = (0, node_path_1.join)(dir, 'events.jsonl');
|
|
42
|
+
if ((0, node_fs_1.existsSync)(ev))
|
|
43
|
+
this.trustEvents = (0, node_fs_1.readFileSync)(ev, 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
|
|
44
|
+
this.loading = false;
|
|
45
|
+
}
|
|
46
|
+
flush() {
|
|
47
|
+
const rows = [...this.rows.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : a.version < b.version ? -1 : 1));
|
|
48
|
+
atomicWrite((0, node_path_1.join)(this.dir, 'tools.jsonl'), rows.map((r) => JSON.stringify(r)).join('\n') + (rows.length ? '\n' : ''));
|
|
49
|
+
atomicWrite((0, node_path_1.join)(this.dir, 'index.json'), JSON.stringify({ snapshot_id: this.snapshotId(), count: rows.length, entries: this.index() }, null, 1) + '\n');
|
|
50
|
+
}
|
|
51
|
+
upsert(rows) {
|
|
52
|
+
const r = super.upsert(rows);
|
|
53
|
+
if (!this.loading)
|
|
54
|
+
this.flush();
|
|
55
|
+
return r;
|
|
56
|
+
}
|
|
57
|
+
setTrust(id, version, to, by, why) {
|
|
58
|
+
super.setTrust(id, version, to, by, why);
|
|
59
|
+
this.flush();
|
|
60
|
+
}
|
|
61
|
+
recordEvent(e) {
|
|
62
|
+
super.recordEvent(e);
|
|
63
|
+
if (!this.loading)
|
|
64
|
+
(0, node_fs_1.appendFileSync)((0, node_path_1.join)(this.dir, 'events.jsonl'), JSON.stringify(e) + '\n');
|
|
65
|
+
}
|
|
66
|
+
writeRaw(sha, text) {
|
|
67
|
+
if (!/^[0-9a-f]{64}$/.test(sha))
|
|
68
|
+
throw new Error('bad sha');
|
|
69
|
+
const d = (0, node_path_1.join)(this.dir, 'raw');
|
|
70
|
+
(0, node_fs_1.mkdirSync)(d, { recursive: true });
|
|
71
|
+
const p = (0, node_path_1.join)(d, `${sha}.txt`);
|
|
72
|
+
if (!(0, node_fs_1.existsSync)(p))
|
|
73
|
+
atomicWrite(p, text);
|
|
74
|
+
}
|
|
75
|
+
vectors(model) {
|
|
76
|
+
const mem = super.vectors(model);
|
|
77
|
+
if (mem)
|
|
78
|
+
return mem;
|
|
79
|
+
if (!MODEL_RE.test(model))
|
|
80
|
+
return null;
|
|
81
|
+
const meta = (0, node_path_1.join)(this.dir, `vectors.${model}.json`);
|
|
82
|
+
const bin = (0, node_path_1.join)(this.dir, `vectors.${model}.f32`);
|
|
83
|
+
if (!(0, node_fs_1.existsSync)(meta) || !(0, node_fs_1.existsSync)(bin))
|
|
84
|
+
return null;
|
|
85
|
+
const m = JSON.parse((0, node_fs_1.readFileSync)(meta, 'utf8'));
|
|
86
|
+
const buf = (0, node_fs_1.readFileSync)(bin);
|
|
87
|
+
const data = new Float32Array(buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength));
|
|
88
|
+
if (m.model !== model || data.length !== m.dims * m.ids.length)
|
|
89
|
+
throw new Error(`vectors.${model} do not match their metadata`);
|
|
90
|
+
const v = { dims: m.dims, ids: m.ids, data };
|
|
91
|
+
super.setVectors(model, v);
|
|
92
|
+
return v;
|
|
93
|
+
}
|
|
94
|
+
writeVectors(model, v, builtAt) {
|
|
95
|
+
if (!MODEL_RE.test(model))
|
|
96
|
+
throw new Error('bad model name');
|
|
97
|
+
super.setVectors(model, v);
|
|
98
|
+
atomicWrite((0, node_path_1.join)(this.dir, `vectors.${model}.f32`), Buffer.from(v.data.buffer, v.data.byteOffset, v.data.byteLength));
|
|
99
|
+
atomicWrite((0, node_path_1.join)(this.dir, `vectors.${model}.json`), JSON.stringify({ model, dims: v.dims, count: v.ids.length, built_at: builtAt, ids: v.ids }) + '\n');
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.FileCatalogStore = FileCatalogStore;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { RawToolDescriptor } from '../adapters/raw';
|
|
2
|
+
import type { CatalogStore, TrustEvent } from '../types/ports';
|
|
3
|
+
import type { ToolRecord } from '../types/tool';
|
|
4
|
+
import { type SanitiseFlag } from './sanitise';
|
|
5
|
+
export declare const MAX_SCHEMA_BYTES: number;
|
|
6
|
+
export declare const MAX_OPS = 200;
|
|
7
|
+
export declare const MAX_SAME_AS_GROUP = 8;
|
|
8
|
+
export interface Revocation {
|
|
9
|
+
id: string;
|
|
10
|
+
version: string | '*';
|
|
11
|
+
why: string;
|
|
12
|
+
by: string;
|
|
13
|
+
at: string;
|
|
14
|
+
}
|
|
15
|
+
export interface IngestOpts {
|
|
16
|
+
/** ISO time stamped on rows as ingested_at (not part of the snapshot id). */
|
|
17
|
+
now: string;
|
|
18
|
+
revoked?: Revocation[];
|
|
19
|
+
/** Capability tagger (S2): returns capability ids for an operation's text. */
|
|
20
|
+
tagger?: (text: string) => string[];
|
|
21
|
+
/** Maximum rows kept per source (T14). */
|
|
22
|
+
sourceCaps?: Record<string, number>;
|
|
23
|
+
/** Raw text sink (catalog/raw/<sha>.txt in the file store). */
|
|
24
|
+
rawSink?: (sha: string, text: string) => void;
|
|
25
|
+
}
|
|
26
|
+
export interface SourceCount {
|
|
27
|
+
seen: number;
|
|
28
|
+
added: number;
|
|
29
|
+
versioned: number;
|
|
30
|
+
unchanged: number;
|
|
31
|
+
rejected: number;
|
|
32
|
+
capped: number;
|
|
33
|
+
}
|
|
34
|
+
export interface IngestReport {
|
|
35
|
+
per_source: Record<string, SourceCount>;
|
|
36
|
+
quarantined: number;
|
|
37
|
+
verified: number;
|
|
38
|
+
rejected: Array<{
|
|
39
|
+
source: string;
|
|
40
|
+
source_ref: string;
|
|
41
|
+
why: string;
|
|
42
|
+
}>;
|
|
43
|
+
sanitise_flags: Partial<Record<SanitiseFlag, number>>;
|
|
44
|
+
same_as_links: number;
|
|
45
|
+
same_as_groups_skipped: number;
|
|
46
|
+
revoked_applied: number;
|
|
47
|
+
events: TrustEvent[];
|
|
48
|
+
snapshot_id: string;
|
|
49
|
+
}
|
|
50
|
+
export declare function toRecord(r: RawToolDescriptor, opts: IngestOpts, flagSink: (f: SanitiseFlag[]) => void): ToolRecord | {
|
|
51
|
+
reject: string;
|
|
52
|
+
};
|
|
53
|
+
export declare function ingest(raws: RawToolDescriptor[], store: CatalogStore, opts: IngestOpts): IngestReport;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_SAME_AS_GROUP = exports.MAX_OPS = exports.MAX_SCHEMA_BYTES = void 0;
|
|
4
|
+
exports.toRecord = toRecord;
|
|
5
|
+
exports.ingest = ingest;
|
|
6
|
+
/**
|
|
7
|
+
* Ingestion (plan.md S1): raw descriptors to ToolRecords in a CatalogStore.
|
|
8
|
+
*
|
|
9
|
+
* 1. validate: at most 200 operations, schemas at most 64 KB each (T14), a valid id (ADR-004)
|
|
10
|
+
* 2. sanitise text (T1); raw text kept by hash only
|
|
11
|
+
* 3. risk classes per operation (fail closed); a payment operation taking a destination is refused (security.txt 1)
|
|
12
|
+
* 4. schema hash and implementation hash (no probe)
|
|
13
|
+
* 5. dedup and versions: same id + same version + same schema = unchanged; a new provider version or a new schema =
|
|
14
|
+
* a new version row; same endpoint host + same known schema under another id = same_as link
|
|
15
|
+
* 6. trust: native Kognai rows from Kognai sources are verified; everything else is quarantined; a reviewed or
|
|
16
|
+
* verified outside row whose hashes change goes back to quarantine with an event
|
|
17
|
+
* 7. revocation list applied last
|
|
18
|
+
* 8. report: counts per source, quarantined, rejected with reasons, sanitise flags
|
|
19
|
+
*
|
|
20
|
+
* Deterministic: input order does not matter (sorted), and nothing time based enters the snapshot id.
|
|
21
|
+
*/
|
|
22
|
+
const hash_1 = require("../hash");
|
|
23
|
+
const ids_1 = require("../ids");
|
|
24
|
+
const tool_1 = require("../types/tool");
|
|
25
|
+
const lifecycle_1 = require("./lifecycle");
|
|
26
|
+
const risk_1 = require("./risk");
|
|
27
|
+
const sanitise_1 = require("./sanitise");
|
|
28
|
+
const validate_1 = require("../schema/validate");
|
|
29
|
+
exports.MAX_SCHEMA_BYTES = 64 * 1024;
|
|
30
|
+
exports.MAX_OPS = 200;
|
|
31
|
+
exports.MAX_SAME_AS_GROUP = 8;
|
|
32
|
+
const KOGNAI_SOURCE = /^kognai:/;
|
|
33
|
+
/** Operations that move value somewhere (T15), as opposed to reading about tokens. */
|
|
34
|
+
const TRANSFER_CUE = /\b(transfer|send|withdraw|payout|pay out|remit|disburse)\b/;
|
|
35
|
+
const cleanName = (s) => (0, sanitise_1.sanitiseShort)(s, 120) || 'unnamed';
|
|
36
|
+
const hostOf = (u) => { try {
|
|
37
|
+
return u ? new URL(u).hostname.toLowerCase() : null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
} };
|
|
42
|
+
function implHash(r) {
|
|
43
|
+
if (r.implementation_hash)
|
|
44
|
+
return r.implementation_hash;
|
|
45
|
+
if (r.package)
|
|
46
|
+
return (0, hash_1.sha256)(`${r.package.registry}:${r.package.identifier}@${r.package.version || '?'}`);
|
|
47
|
+
const h = hostOf(r.endpoint);
|
|
48
|
+
return h ? (0, hash_1.sha256)(`endpoint:${h}`) : null;
|
|
49
|
+
}
|
|
50
|
+
function toRecord(r, opts, flagSink) {
|
|
51
|
+
if (!Array.isArray(r.operations))
|
|
52
|
+
return { reject: 'no operations array' };
|
|
53
|
+
if (r.operations.length > exports.MAX_OPS)
|
|
54
|
+
return { reject: `more than ${exports.MAX_OPS} operations (T14)` };
|
|
55
|
+
let id;
|
|
56
|
+
try {
|
|
57
|
+
id = (0, ids_1.formatToolId)(r.origin, r.path);
|
|
58
|
+
}
|
|
59
|
+
catch (e) {
|
|
60
|
+
return { reject: `bad id: ${e.message}` };
|
|
61
|
+
}
|
|
62
|
+
const native = r.origin === 'kognai' && KOGNAI_SOURCE.test(r.source);
|
|
63
|
+
if (r.origin === 'kognai' && !native)
|
|
64
|
+
return { reject: 'origin kognai claimed by a non Kognai source' };
|
|
65
|
+
const desc = (0, sanitise_1.sanitise)(r.description || '');
|
|
66
|
+
flagSink(desc.flags);
|
|
67
|
+
const rawSha = (0, hash_1.sha256)(String(r.description || ''));
|
|
68
|
+
opts.rawSink?.(rawSha, String(r.description || ''));
|
|
69
|
+
const externalNet = !native || !!hostOf(r.endpoint);
|
|
70
|
+
const ops = [];
|
|
71
|
+
const seenOps = new Set();
|
|
72
|
+
for (const o of r.operations) {
|
|
73
|
+
const op = String(o.op || 'call').toLowerCase().replace(/[^a-z0-9_.-]+/g, '_').slice(0, 64) || 'call';
|
|
74
|
+
if (seenOps.has(op))
|
|
75
|
+
continue;
|
|
76
|
+
seenOps.add(op);
|
|
77
|
+
for (const s of [o.input_schema, o.output_schema]) {
|
|
78
|
+
if (s && Buffer.byteLength((0, hash_1.canonical)(s)) > exports.MAX_SCHEMA_BYTES)
|
|
79
|
+
return { reject: `schema of ${op} over 64 KB (T14)` };
|
|
80
|
+
}
|
|
81
|
+
const summaryRaw = o.summary || r.name;
|
|
82
|
+
const sum = (0, sanitise_1.sanitise)(summaryRaw);
|
|
83
|
+
flagSink(sum.flags);
|
|
84
|
+
const text = native ? sum.text : `${r.name} ${desc.text} ${sum.text}`;
|
|
85
|
+
const risk = (0, risk_1.deriveRisk)({ origin: r.origin, side_effects: o.side_effects, scopes: o.scopes, text, input_schema: o.input_schema, external_network: externalNet });
|
|
86
|
+
// a declared read is not a transfer; a declared payment, or unknown side effects with a transfer cue, is
|
|
87
|
+
const movesMoney = risk.side_effects.includes('payment') || (risk.side_effects.length === 0 && TRANSFER_CUE.test(text.toLowerCase()));
|
|
88
|
+
if (risk.risk.includes('payment') && movesMoney && (0, risk_1.takesDestination)(o.input_schema))
|
|
89
|
+
return { reject: `operation ${op} moves money to an address it takes as input (security.txt rule 1, T15)` };
|
|
90
|
+
// explicit capabilities (native sources) are authoritative; only untagged operations are tagged from text
|
|
91
|
+
const tagged = opts.tagger && !(o.capabilities && o.capabilities.length) ? opts.tagger(`${cleanName(r.name)} ${sum.text} ${native ? '' : desc.text} ${(r.tags || []).join(' ')}`) : [];
|
|
92
|
+
ops.push({
|
|
93
|
+
op,
|
|
94
|
+
summary: (0, sanitise_1.sanitiseShort)(summaryRaw, 200),
|
|
95
|
+
capabilities: [...new Set([...(o.capabilities || []), ...tagged])].sort(),
|
|
96
|
+
input_schema: (o.input_schema && typeof o.input_schema === 'object') ? o.input_schema : {},
|
|
97
|
+
output_schema: (o.output_schema && typeof o.output_schema === 'object') ? o.output_schema : null,
|
|
98
|
+
side_effects: risk.side_effects,
|
|
99
|
+
risk_classes: risk.risk,
|
|
100
|
+
scopes: [...(o.scopes || [])],
|
|
101
|
+
idempotent: o.idempotent ?? null,
|
|
102
|
+
url_from_user: risk.url_from_user,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const sh = (0, hash_1.schemaHash)(ops);
|
|
106
|
+
const providerVersion = r.version && /^[0-9A-Za-z.+_-]{1,40}$/.test(r.version) ? r.version : null;
|
|
107
|
+
const version = providerVersion || `0.0.0+${sh.slice(0, 12)}`;
|
|
108
|
+
const privacy = r.privacy_class || 'public';
|
|
109
|
+
const tier = r.business_tier === undefined ? null : r.business_tier;
|
|
110
|
+
const rec = {
|
|
111
|
+
schema_version: 1,
|
|
112
|
+
id,
|
|
113
|
+
version,
|
|
114
|
+
name: cleanName(r.name),
|
|
115
|
+
description_safe: desc.text,
|
|
116
|
+
description_raw_sha256: rawSha,
|
|
117
|
+
provider: { name: cleanName(r.provider?.name || 'unknown'), ...(r.provider?.url ? { url: String(r.provider.url).slice(0, 300) } : {}), kognai_owned: native && r.provider?.kognai_owned !== false },
|
|
118
|
+
origin: r.origin,
|
|
119
|
+
category: String(r.category || 'other').toLowerCase().slice(0, 60),
|
|
120
|
+
tags: [...new Set([...(r.tags || []).map((t) => String(t).toLowerCase().slice(0, 60)), ...(r.founder_approval ? ['founder_approval'] : [])])].slice(0, 40),
|
|
121
|
+
operations: ops,
|
|
122
|
+
auth: r.auth ? { ...r.auth } : { kind: 'none' },
|
|
123
|
+
privacy_class: privacy,
|
|
124
|
+
business_tier: tier,
|
|
125
|
+
regions: r.regions && r.regions.length ? [...r.regions] : ['*'],
|
|
126
|
+
networks: [...new Set(r.networks || [])],
|
|
127
|
+
pricing: r.pricing ? { ...r.pricing } : { model: 'unknown', source: 'unknown' },
|
|
128
|
+
success: { ...tool_1.EMPTY_SUCCESS },
|
|
129
|
+
latency: { p50_ms: null, p95_ms: null, n: 0 },
|
|
130
|
+
rate_limits: null,
|
|
131
|
+
health: { state: 'unknown', checked_at: null, source: 'none' },
|
|
132
|
+
dependencies: [...(r.dependencies || [])],
|
|
133
|
+
trust_state: native ? 'verified' : 'quarantined',
|
|
134
|
+
skill_ids: [...(r.skill_ids || [])],
|
|
135
|
+
last_validated: null,
|
|
136
|
+
schema_hash: sh,
|
|
137
|
+
implementation_hash: implHash(r),
|
|
138
|
+
lifecycle: r.lifecycle || 'active',
|
|
139
|
+
aliases: [...new Set(r.aliases || [])],
|
|
140
|
+
same_as: [],
|
|
141
|
+
...(r.product_id ? { product_id: r.product_id } : {}),
|
|
142
|
+
...(r.aiax_ready === true ? { aiax_ready: true } : {}),
|
|
143
|
+
provenance: [{
|
|
144
|
+
source: r.source, source_ref: String(r.source_ref).slice(0, 400), retrieved_at: r.provenance.retrieved_at,
|
|
145
|
+
licence: r.provenance.licence ?? r.licence ?? null, adapter: r.provenance.adapter, adapter_version: r.provenance.adapter_version,
|
|
146
|
+
snapshot_sha256: r.provenance.snapshot_sha256,
|
|
147
|
+
}],
|
|
148
|
+
ingested_at: opts.now,
|
|
149
|
+
};
|
|
150
|
+
const v = (0, validate_1.validateToolRecord)(rec);
|
|
151
|
+
if (!v.ok)
|
|
152
|
+
return { reject: `schema: ${v.errors.slice(0, 3).join('; ')}` };
|
|
153
|
+
return rec;
|
|
154
|
+
}
|
|
155
|
+
const knownSchema = (r) => r.operations.some((o) => Object.keys(o.input_schema || {}).length > 0);
|
|
156
|
+
function ingest(raws, store, opts) {
|
|
157
|
+
const report = { per_source: {}, quarantined: 0, verified: 0, rejected: [], sanitise_flags: {}, same_as_links: 0, same_as_groups_skipped: 0, revoked_applied: 0, events: [], snapshot_id: '' };
|
|
158
|
+
const count = (s) => (report.per_source[s] ||= { seen: 0, added: 0, versioned: 0, unchanged: 0, rejected: 0, capped: 0 });
|
|
159
|
+
const flagSink = (fs) => { for (const f of fs)
|
|
160
|
+
report.sanitise_flags[f] = (report.sanitise_flags[f] || 0) + 1; };
|
|
161
|
+
const sorted = [...raws].sort((a, b) => (`${a.source}\u0000${a.path}\u0000${a.source_ref}` < `${b.source}\u0000${b.path}\u0000${b.source_ref}` ? -1 : 1));
|
|
162
|
+
const keptPerSource = new Map();
|
|
163
|
+
const batch = new Map(); // id -> record (latest in this batch)
|
|
164
|
+
for (const raw of sorted) {
|
|
165
|
+
const c = count(raw.source);
|
|
166
|
+
c.seen++;
|
|
167
|
+
const cap = opts.sourceCaps?.[raw.source];
|
|
168
|
+
if (cap !== undefined && (keptPerSource.get(raw.source) || 0) >= cap) {
|
|
169
|
+
c.capped++;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const rec = toRecord(raw, opts, flagSink);
|
|
173
|
+
if ('reject' in rec) {
|
|
174
|
+
c.rejected++;
|
|
175
|
+
report.rejected.push({ source: raw.source, source_ref: raw.source_ref, why: rec.reject });
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
keptPerSource.set(raw.source, (keptPerSource.get(raw.source) || 0) + 1);
|
|
179
|
+
const prevInBatch = batch.get(rec.id);
|
|
180
|
+
if (prevInBatch) {
|
|
181
|
+
// two descriptors with one id in one batch: the first stays, the second is reported (not merged silently)
|
|
182
|
+
c.rejected++;
|
|
183
|
+
report.rejected.push({ source: raw.source, source_ref: raw.source_ref, why: `duplicate id ${rec.id} in this batch` });
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
// versions against the store
|
|
187
|
+
const prev = store.get(rec.id);
|
|
188
|
+
if (prev) {
|
|
189
|
+
if (prev.version === rec.version && prev.schema_hash !== rec.schema_hash)
|
|
190
|
+
rec.version = `${rec.version.split('+')[0]}+${rec.schema_hash.slice(0, 12)}`;
|
|
191
|
+
const drift = (0, lifecycle_1.trustAfterDrift)(prev, rec);
|
|
192
|
+
if (rec.origin !== 'kognai') {
|
|
193
|
+
const carried = prev.trust_state === 'revoked' ? 'revoked' : drift.drifted ? drift.trust : prev.trust_state;
|
|
194
|
+
if (drift.drifted && (prev.trust_state === 'verified' || prev.trust_state === 'reviewed')) {
|
|
195
|
+
const e = { at: opts.now, id: rec.id, version: rec.version, from: prev.trust_state, to: 'quarantined', by: 'ktr-ingest', why: 'hash drift: schema or implementation changed' };
|
|
196
|
+
report.events.push(e);
|
|
197
|
+
store.recordEvent(e);
|
|
198
|
+
}
|
|
199
|
+
rec.trust_state = carried;
|
|
200
|
+
}
|
|
201
|
+
rec.same_as = [...prev.same_as];
|
|
202
|
+
}
|
|
203
|
+
batch.set(rec.id, rec);
|
|
204
|
+
}
|
|
205
|
+
// same_as: same endpoint host and same known schema under another id
|
|
206
|
+
const all = new Map();
|
|
207
|
+
for (const r of store.latest())
|
|
208
|
+
all.set(r.id, r);
|
|
209
|
+
for (const r of batch.values())
|
|
210
|
+
all.set(r.id, r);
|
|
211
|
+
const byKey = new Map();
|
|
212
|
+
for (const r of all.values()) {
|
|
213
|
+
// native rows share a source file hash, not an endpoint: they are never duplicates of each other
|
|
214
|
+
if (r.origin === 'kognai' || !knownSchema(r) || !r.implementation_hash)
|
|
215
|
+
continue;
|
|
216
|
+
const k = `${r.implementation_hash}|${r.schema_hash}`;
|
|
217
|
+
(byKey.get(k) || byKey.set(k, []).get(k)).push(r.id);
|
|
218
|
+
}
|
|
219
|
+
for (const ids of byKey.values()) {
|
|
220
|
+
if (ids.length < 2)
|
|
221
|
+
continue;
|
|
222
|
+
// a large group is one templated endpoint (a token address in the path): not duplicates of one tool
|
|
223
|
+
if (ids.length > exports.MAX_SAME_AS_GROUP) {
|
|
224
|
+
report.same_as_groups_skipped++;
|
|
225
|
+
continue;
|
|
226
|
+
}
|
|
227
|
+
for (const id of ids) {
|
|
228
|
+
const r = batch.get(id);
|
|
229
|
+
if (!r)
|
|
230
|
+
continue;
|
|
231
|
+
const before = r.same_as.length;
|
|
232
|
+
r.same_as = [...new Set([...r.same_as, ...ids.filter((x) => x !== id)])].sort();
|
|
233
|
+
report.same_as_links += r.same_as.length - before;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// revocation last
|
|
237
|
+
for (const rv of opts.revoked || []) {
|
|
238
|
+
const r = batch.get(rv.id);
|
|
239
|
+
if (r && (rv.version === '*' || rv.version === r.version) && r.trust_state !== 'revoked') {
|
|
240
|
+
const e = { at: rv.at, id: r.id, version: r.version, from: r.trust_state, to: 'revoked', by: rv.by, why: rv.why };
|
|
241
|
+
r.trust_state = 'revoked';
|
|
242
|
+
report.revoked_applied++;
|
|
243
|
+
report.events.push(e);
|
|
244
|
+
store.recordEvent(e);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
// a revocation of a stored row that is not in this batch
|
|
248
|
+
for (const rv of opts.revoked || []) {
|
|
249
|
+
if (batch.has(rv.id))
|
|
250
|
+
continue;
|
|
251
|
+
const r = store.get(rv.id);
|
|
252
|
+
if (r && (rv.version === '*' || rv.version === r.version) && r.trust_state !== 'revoked') {
|
|
253
|
+
store.setTrust(r.id, r.version, 'revoked', rv.by, rv.why);
|
|
254
|
+
report.revoked_applied++;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const rows = [...batch.values()].sort((a, b) => (a.id < b.id ? -1 : 1));
|
|
258
|
+
const before = new Map(rows.map((r) => [r.id, store.get(r.id)]));
|
|
259
|
+
store.upsert(rows);
|
|
260
|
+
for (const r of rows) {
|
|
261
|
+
const c = count(r.provenance[0].source);
|
|
262
|
+
const p = before.get(r.id);
|
|
263
|
+
if (!p)
|
|
264
|
+
c.added++;
|
|
265
|
+
else if (p.version === r.version)
|
|
266
|
+
c.unchanged++;
|
|
267
|
+
else
|
|
268
|
+
c.versioned++;
|
|
269
|
+
if (r.trust_state === 'quarantined')
|
|
270
|
+
report.quarantined++;
|
|
271
|
+
if (r.trust_state === 'verified')
|
|
272
|
+
report.verified++;
|
|
273
|
+
}
|
|
274
|
+
report.snapshot_id = store.snapshotId();
|
|
275
|
+
return report;
|
|
276
|
+
}
|