aptly 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.en.md +45 -0
- package/README.ja.md +45 -0
- package/README.md +45 -0
- package/README.zh-CN.md +45 -0
- package/dist/bin/aptly.d.ts +2 -0
- package/dist/bin/aptly.js +122 -0
- package/dist/src/automatic-hook.d.ts +42 -0
- package/dist/src/automatic-hook.js +232 -0
- package/dist/src/catalog.d.ts +19 -0
- package/dist/src/catalog.js +135 -0
- package/dist/src/config.d.ts +24 -0
- package/dist/src/config.js +75 -0
- package/dist/src/core.d.ts +50 -0
- package/dist/src/core.js +324 -0
- package/dist/src/diagnostics.d.ts +11 -0
- package/dist/src/diagnostics.js +68 -0
- package/dist/src/files.d.ts +10 -0
- package/dist/src/files.js +70 -0
- package/dist/src/hook.d.ts +20 -0
- package/dist/src/hook.js +102 -0
- package/dist/src/inspect.d.ts +31 -0
- package/dist/src/inspect.js +219 -0
- package/dist/src/model-info.d.ts +32 -0
- package/dist/src/model-info.js +223 -0
- package/dist/src/provenance.d.ts +62 -0
- package/dist/src/provenance.js +266 -0
- package/dist/src/provider.d.ts +9 -0
- package/dist/src/provider.js +80 -0
- package/dist/src/report.d.ts +51 -0
- package/dist/src/report.js +177 -0
- package/dist/src/runtime.d.ts +8 -0
- package/dist/src/runtime.js +22 -0
- package/dist/src/server.d.ts +7 -0
- package/dist/src/server.js +80 -0
- package/dist/src/setup.d.ts +22 -0
- package/dist/src/setup.js +408 -0
- package/dist/src/types.d.ts +76 -0
- package/dist/src/types.js +2 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +1 -0
- package/docs/ADVISORY_USAGE.md +196 -0
- package/docs/DRAFT_0_1_0.md +55 -0
- package/docs/guide/en.md +190 -0
- package/docs/guide/ja.md +190 -0
- package/docs/guide/ko.md +190 -0
- package/docs/guide/zh-CN.md +190 -0
- package/examples/claude-mcp.json +15 -0
- package/examples/codex-mcp.toml +9 -0
- package/examples/recommend.claude.json +9 -0
- package/examples/recommend.codex.json +9 -0
- package/examples/router.shadow.json +5 -0
- package/examples/settings-inventory.json +5 -0
- package/package.json +75 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
const codexSource = 'https://learn.chatgpt.com/docs/models';
|
|
3
|
+
const claudeSource = 'https://code.claude.com/docs/en/model-config.md';
|
|
4
|
+
const entries = [
|
|
5
|
+
{ profile: { id: 'codex-simple', client: 'codex', model: 'gpt-6-luna', reasoning_effort: 'max' }, tier: 'simple',
|
|
6
|
+
task: 'Bounded extraction, format conversion, wording cleanup, or small mechanical edits.',
|
|
7
|
+
fact: 'Efficient for focused, repeatable and high-volume tasks.', source: codexSource, default: true },
|
|
8
|
+
{ profile: { id: 'codex-routine', client: 'codex', model: 'gpt-6.1-terra', reasoning_effort: 'high' }, tier: 'standard',
|
|
9
|
+
task: 'Context-aware writing or translation, everyday analysis, or clearly scoped implementation.',
|
|
10
|
+
fact: 'Fast and efficient for writing, visual understanding, and computer use.', source: codexSource, default: true },
|
|
11
|
+
{ profile: { id: 'codex-deep', client: 'codex', model: 'gpt-6.1-sol', reasoning_effort: 'medium' }, tier: 'deep',
|
|
12
|
+
task: 'Cross-component debugging, architectural tradeoffs, or demanding accuracy-sensitive review.',
|
|
13
|
+
fact: 'Near-Astra capability for complex, repeated and long-running work at lower cost than Astra.', source: codexSource, default: true },
|
|
14
|
+
{ profile: { id: 'claude-simple', client: 'claude', model: 'haiku' }, tier: 'simple',
|
|
15
|
+
task: 'Bounded extraction, format conversion, wording cleanup, or small mechanical edits.',
|
|
16
|
+
fact: 'Fast and efficient for simple tasks.', source: claudeSource, default: true },
|
|
17
|
+
{ profile: { id: 'claude-routine', client: 'claude', model: 'sonnet' }, tier: 'standard',
|
|
18
|
+
task: 'Context-aware writing or translation, everyday analysis, or clearly scoped implementation.',
|
|
19
|
+
fact: 'General-purpose model family for daily coding tasks.', source: claudeSource, default: true },
|
|
20
|
+
{ profile: { id: 'claude-deep', client: 'claude', model: 'opus' }, tier: 'deep',
|
|
21
|
+
task: 'Cross-component debugging, architectural tradeoffs, or demanding accuracy-sensitive review.',
|
|
22
|
+
fact: 'Model family for complex reasoning tasks.', source: claudeSource, default: true },
|
|
23
|
+
{ profile: { id: 'codex-frontier', client: 'codex', model: 'gpt-6-astra', reasoning_effort: 'high' }, tier: 'deep',
|
|
24
|
+
task: 'The most demanding sustained end-to-end workflows and judgment across several sources.',
|
|
25
|
+
fact: 'Most capable family for complex work across code, apps and research.', source: codexSource, default: false },
|
|
26
|
+
{ profile: { id: 'claude-frontier', client: 'claude', model: 'fable' }, tier: 'deep',
|
|
27
|
+
task: 'The hardest and longest-running workflows requiring sustained investigation and judgment.',
|
|
28
|
+
fact: 'Model family for the hardest and longest-running tasks.', source: claudeSource, default: false },
|
|
29
|
+
];
|
|
30
|
+
for (const entry of entries) {
|
|
31
|
+
Object.freeze(entry.profile);
|
|
32
|
+
Object.freeze(entry);
|
|
33
|
+
}
|
|
34
|
+
Object.freeze(entries);
|
|
35
|
+
const clientOrder = ['codex', 'claude'];
|
|
36
|
+
const own = (value, key) => Object.hasOwn(value, key);
|
|
37
|
+
const plain = (value) => value !== null && typeof value === 'object' &&
|
|
38
|
+
!Array.isArray(value) && [Object.prototype, null].includes(Object.getPrototypeOf(value));
|
|
39
|
+
const error = (code) => Object.assign(new Error(code), { code });
|
|
40
|
+
/** Fresh copies prevent caller mutation from changing the trusted registry. */
|
|
41
|
+
export function builtInProfiles() { return entries.filter(entry => entry.default).map(entry => ({ ...entry.profile })); }
|
|
42
|
+
/** Exact finite profile tuples, including key coverage, not arbitrary model strings. */
|
|
43
|
+
export function isSupportedProfile(value) {
|
|
44
|
+
try {
|
|
45
|
+
if (!plain(value) || Object.getOwnPropertySymbols(value).length)
|
|
46
|
+
return false;
|
|
47
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
48
|
+
if (Object.values(descriptors).some(d => !own(d, 'value') || !d.enumerable))
|
|
49
|
+
return false;
|
|
50
|
+
return entries.some(({ profile }) => {
|
|
51
|
+
const expected = Object.entries(profile);
|
|
52
|
+
return Object.keys(descriptors).length === expected.length && expected.every(([key, field]) => own(descriptors, key) && descriptors[key].value === field);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function envValue(env, key) {
|
|
60
|
+
const descriptor = Object.getOwnPropertyDescriptor(env, key);
|
|
61
|
+
if (!descriptor)
|
|
62
|
+
return undefined;
|
|
63
|
+
if (!own(descriptor, 'value') || typeof descriptor.value !== 'string')
|
|
64
|
+
throw error('catalog_configuration_invalid');
|
|
65
|
+
return descriptor.value;
|
|
66
|
+
}
|
|
67
|
+
function selectedModels(env, client) {
|
|
68
|
+
const suffix = client === 'claude' ? 'CLAUDE_MODELS' : 'CODEX_MODELS';
|
|
69
|
+
if (own(env, 'APTLY_' + suffix) && own(env, suffix))
|
|
70
|
+
throw error('catalog_configuration_conflict');
|
|
71
|
+
const raw = envValue(env, 'APTLY_' + suffix) ?? envValue(env, suffix);
|
|
72
|
+
if (raw === undefined)
|
|
73
|
+
return entries.filter(entry => entry.default && entry.profile.client === client).map(entry => entry.profile.model);
|
|
74
|
+
let parsed;
|
|
75
|
+
try {
|
|
76
|
+
parsed = JSON.parse(raw);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
throw error('catalog_configuration_invalid');
|
|
80
|
+
}
|
|
81
|
+
if (!Array.isArray(parsed) || parsed.length > 20 || parsed.some(model => typeof model !== 'string') ||
|
|
82
|
+
new Set(parsed).size !== parsed.length || parsed.some(model => !entries.some(entry => entry.profile.client === client && entry.profile.model === model))) {
|
|
83
|
+
throw error('catalog_configuration_invalid');
|
|
84
|
+
}
|
|
85
|
+
return parsed;
|
|
86
|
+
}
|
|
87
|
+
function normalizedFacts(facts) {
|
|
88
|
+
const result = new Map();
|
|
89
|
+
if (!Array.isArray(facts) || facts.length > 40)
|
|
90
|
+
throw error('catalog_facts_invalid');
|
|
91
|
+
for (const fact of facts) {
|
|
92
|
+
if (!plain(fact) || Object.getOwnPropertySymbols(fact).length)
|
|
93
|
+
throw error('catalog_facts_invalid');
|
|
94
|
+
const descriptors = Object.getOwnPropertyDescriptors(fact);
|
|
95
|
+
if (Object.keys(descriptors).length !== 3 || ['client', 'model', 'description'].some(key => !own(descriptors, key) ||
|
|
96
|
+
!own(descriptors[key], 'value') || !descriptors[key].enumerable))
|
|
97
|
+
throw error('catalog_facts_invalid');
|
|
98
|
+
const client = descriptors.client.value, model = descriptors.model.value, description = descriptors.description.value;
|
|
99
|
+
if ((client !== 'codex' && client !== 'claude') || typeof model !== 'string' || !/^[a-z0-9.-]{1,80}$/.test(model) ||
|
|
100
|
+
typeof description !== 'string' || !description.trim() || Buffer.byteLength(description) > 800 ||
|
|
101
|
+
/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(description) ||
|
|
102
|
+
/\bsk-[A-Za-z0-9_-]{12,}|\bBearer\s+[A-Za-z0-9._~+\/-]{8,}={0,2}/i.test(description))
|
|
103
|
+
throw error('catalog_facts_invalid');
|
|
104
|
+
if (!entries.some(entry => entry.profile.client === client && entry.profile.model === model))
|
|
105
|
+
continue;
|
|
106
|
+
const key = client + ':' + model;
|
|
107
|
+
if (result.has(key))
|
|
108
|
+
throw error('catalog_facts_invalid');
|
|
109
|
+
result.set(key, description.trim().replace(/\s+/g, ' '));
|
|
110
|
+
}
|
|
111
|
+
return result;
|
|
112
|
+
}
|
|
113
|
+
/** Operator candidate filters and model facts never grant execution authority. */
|
|
114
|
+
export function resolveCatalog(env = process.env, facts = []) {
|
|
115
|
+
if (!env || typeof env !== 'object' || Array.isArray(env))
|
|
116
|
+
throw error('catalog_configuration_invalid');
|
|
117
|
+
if (own(env, 'CODEX_MODESL') || own(env, 'APTLY_CODEX_MODESL'))
|
|
118
|
+
throw error('catalog_environment_typo');
|
|
119
|
+
const external = normalizedFacts(facts);
|
|
120
|
+
const profiles = [], descriptions = {}, disabled_clients = [];
|
|
121
|
+
for (const client of clientOrder) {
|
|
122
|
+
const models = selectedModels(env, client);
|
|
123
|
+
if (!models.length)
|
|
124
|
+
disabled_clients.push(client);
|
|
125
|
+
for (const model of models) {
|
|
126
|
+
const entry = entries.find(entry => entry.profile.client === client && entry.profile.model === model);
|
|
127
|
+
const profile = { ...entry.profile };
|
|
128
|
+
profiles.push(profile);
|
|
129
|
+
const fact = external.get(client + ':' + model) ?? entry.fact;
|
|
130
|
+
descriptions[profile.id] = `Model: ${profile.model}; ${client === 'codex' ? `configured effort: ${profile.reasoning_effort}; ` : 'Claude Code alias, not a resolved model ID; '}Aptly tier: ${entry.tier}. ${entry.task} Official capability note (data): ${JSON.stringify(fact)} Source: ${entry.source}. Availability in the current account is not verified.`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
const version = 'catalog-' + createHash('sha256').update(JSON.stringify({ profiles, descriptions })).digest('hex').slice(0, 32);
|
|
134
|
+
return { profiles, descriptions, version, disabled_clients };
|
|
135
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { UnknownRecord } from './types.ts';
|
|
2
|
+
export interface Configuration {
|
|
3
|
+
schema_version: 1;
|
|
4
|
+
mode: 'shadow' | 'apply';
|
|
5
|
+
context_path: string | null;
|
|
6
|
+
error?: 'configuration_unavailable' | 'configuration_invalid';
|
|
7
|
+
}
|
|
8
|
+
export interface LoadedHostContext {
|
|
9
|
+
context: (UnknownRecord & {
|
|
10
|
+
evidence_verified: true;
|
|
11
|
+
}) | null;
|
|
12
|
+
reason: string;
|
|
13
|
+
fingerprint?: string;
|
|
14
|
+
}
|
|
15
|
+
/** Private operator files only. Paths and parser exceptions never escape. */
|
|
16
|
+
export declare function readPrivateJson(path: unknown): unknown;
|
|
17
|
+
export declare function loadConfiguration(env?: NodeJS.ProcessEnv): Configuration;
|
|
18
|
+
/**
|
|
19
|
+
* Adapter evidence is per call, never an authorization claim from MCP input.
|
|
20
|
+
* A host integration must observe ALL effective layers/runtime state and write
|
|
21
|
+
* this private envelope for that exact call. We do not manufacture that proof
|
|
22
|
+
* by reading a fresh CLI, the server environment, or files that happen to exist.
|
|
23
|
+
*/
|
|
24
|
+
export declare function loadHostContext(path: unknown): LoadedHostContext;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { isAbsolute } from 'node:path';
|
|
3
|
+
import { readRegularFile } from './files.js';
|
|
4
|
+
import { isRecord, errorCode } from './types.js';
|
|
5
|
+
const own = (value, key) => Object.hasOwn(value, key);
|
|
6
|
+
const plain = isRecord;
|
|
7
|
+
const only = (value, keys) => plain(value) && Object.keys(value).every(key => keys.includes(key));
|
|
8
|
+
/** Private operator files only. Paths and parser exceptions never escape. */
|
|
9
|
+
export function readPrivateJson(path) {
|
|
10
|
+
if (typeof path !== 'string' || !isAbsolute(path))
|
|
11
|
+
throw new Error('private_file_invalid');
|
|
12
|
+
try {
|
|
13
|
+
return JSON.parse(readRegularFile(path, { privateFile: true }).toString('utf8'));
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
throw new Error('private_file_unavailable');
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export function loadConfiguration(env = process.env) {
|
|
20
|
+
const defaults = { schema_version: 1, mode: 'shadow', context_path: null };
|
|
21
|
+
if (!env.ROUTER_CONFIG_PATH)
|
|
22
|
+
return defaults;
|
|
23
|
+
let config;
|
|
24
|
+
try {
|
|
25
|
+
config = readPrivateJson(env.ROUTER_CONFIG_PATH);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return { ...defaults, error: 'configuration_unavailable' };
|
|
29
|
+
}
|
|
30
|
+
if (!only(config, ['schema_version', 'mode', 'context_path']) || config.schema_version !== 1 ||
|
|
31
|
+
(config.mode !== 'shadow' && config.mode !== 'apply') ||
|
|
32
|
+
(own(config, 'context_path') && config.context_path !== null && (typeof config.context_path !== 'string' || !isAbsolute(config.context_path)))) {
|
|
33
|
+
return { ...defaults, error: 'configuration_invalid' };
|
|
34
|
+
}
|
|
35
|
+
return { schema_version: 1, mode: config.mode, context_path: typeof config.context_path === 'string' ? config.context_path : null };
|
|
36
|
+
}
|
|
37
|
+
function unchangedSource(source) {
|
|
38
|
+
if (!only(source, ['path', 'sha256']) || typeof source.path !== 'string' || !isAbsolute(source.path) ||
|
|
39
|
+
!(/\.(json|toml|md)$/.test(source.path)) || /(?:^|[/\\])(?:auth|credentials)(?:[.\/\\]|$)/i.test(source.path))
|
|
40
|
+
return false;
|
|
41
|
+
try {
|
|
42
|
+
const bytes = readRegularFile(source.path);
|
|
43
|
+
if (typeof source.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(source.sha256))
|
|
44
|
+
return false;
|
|
45
|
+
return createHash('sha256').update(bytes).digest('hex') === source.sha256;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return source.sha256 === null && errorCode(error) === 'ENOENT';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Adapter evidence is per call, never an authorization claim from MCP input.
|
|
53
|
+
* A host integration must observe ALL effective layers/runtime state and write
|
|
54
|
+
* this private envelope for that exact call. We do not manufacture that proof
|
|
55
|
+
* by reading a fresh CLI, the server environment, or files that happen to exist.
|
|
56
|
+
*/
|
|
57
|
+
export function loadHostContext(path) {
|
|
58
|
+
if (!path)
|
|
59
|
+
return { context: null, reason: 'context_missing' };
|
|
60
|
+
let envelope;
|
|
61
|
+
try {
|
|
62
|
+
envelope = readPrivateJson(path);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return { context: null, reason: 'context_file_unavailable' };
|
|
66
|
+
}
|
|
67
|
+
if (!only(envelope, ['schema_version', 'authority', 'context', 'sources']) || envelope.schema_version !== 1 ||
|
|
68
|
+
envelope.authority !== 'effective-host-settings-v1' || !plain(envelope.context) ||
|
|
69
|
+
own(envelope.context, 'evidence_verified') || !Array.isArray(envelope.sources) || envelope.sources.length > 64) {
|
|
70
|
+
return { context: null, reason: 'context_envelope_invalid' };
|
|
71
|
+
}
|
|
72
|
+
if (!envelope.sources.every(unchangedSource))
|
|
73
|
+
return { context: null, reason: 'context_sources_changed' };
|
|
74
|
+
return { context: { ...envelope.context, evidence_verified: true }, reason: 'host_evidence_loaded', fingerprint: createHash('sha256').update(JSON.stringify(envelope)).digest('hex') };
|
|
75
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Profile, RoutingPolicy, RoutingProvider, RoutingResult, RoutingStatus } from './types.ts';
|
|
2
|
+
export declare const defaultPolicy: Readonly<RoutingPolicy>;
|
|
3
|
+
interface LedgerRecord {
|
|
4
|
+
digest: string;
|
|
5
|
+
state: 'complete' | 'unknown';
|
|
6
|
+
result?: RoutingResult;
|
|
7
|
+
}
|
|
8
|
+
interface LedgerState {
|
|
9
|
+
version: 1;
|
|
10
|
+
calls: number;
|
|
11
|
+
estimated_usd: number;
|
|
12
|
+
days: Record<string, number>;
|
|
13
|
+
requests: Record<string, LedgerRecord>;
|
|
14
|
+
}
|
|
15
|
+
interface RouterOptions {
|
|
16
|
+
policy?: RoutingPolicy;
|
|
17
|
+
provider?: RoutingProvider;
|
|
18
|
+
ledgerPath?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare class Router {
|
|
21
|
+
policy: RoutingPolicy;
|
|
22
|
+
provider: RoutingProvider;
|
|
23
|
+
ledgerPath: string | undefined;
|
|
24
|
+
state: LedgerState;
|
|
25
|
+
active: number;
|
|
26
|
+
constructor({ policy, provider, ledgerPath }?: RouterOptions);
|
|
27
|
+
capabilities(): {
|
|
28
|
+
policy_version: string;
|
|
29
|
+
catalog_version: string;
|
|
30
|
+
profiles: readonly Profile[];
|
|
31
|
+
profile_descriptions: Record<string, string>;
|
|
32
|
+
application: 'advisory_only';
|
|
33
|
+
storage: string;
|
|
34
|
+
cost_guard: string;
|
|
35
|
+
limits: {
|
|
36
|
+
[k: string]: any;
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
result(status: RoutingStatus, reason: string, extra?: Partial<RoutingResult>): RoutingResult;
|
|
40
|
+
validateOutcome(result: unknown): asserts result is RoutingResult;
|
|
41
|
+
load(): void;
|
|
42
|
+
save(): void;
|
|
43
|
+
/** Strict native-routing path: preserves existing host-evidence and digest semantics. */
|
|
44
|
+
recommend(input: unknown): Promise<RoutingResult>;
|
|
45
|
+
/** Independent task advice. Omitted constraints mean no caller-declared pins,
|
|
46
|
+
* not verified host settings. A legacy provenance flag grants no authority. */
|
|
47
|
+
advise(input: unknown): Promise<RoutingResult>;
|
|
48
|
+
private route;
|
|
49
|
+
}
|
|
50
|
+
export {};
|
package/dist/src/core.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { openSync, closeSync, readFileSync, writeFileSync, renameSync, unlinkSync, lstatSync, chmodSync } from 'node:fs';
|
|
3
|
+
import { isRecord, errorCode } from './types.js';
|
|
4
|
+
import { builtInProfiles, resolveCatalog, isSupportedProfile } from './catalog.js';
|
|
5
|
+
const defaultCatalog = resolveCatalog({});
|
|
6
|
+
export const defaultPolicy = Object.freeze({
|
|
7
|
+
version: 'aptly-v010', catalog_version: defaultCatalog.version, max_summary_bytes: 4000,
|
|
8
|
+
max_concurrency: 1, max_daily_calls: 20, max_total_calls: 50,
|
|
9
|
+
estimated_call_usd: 0.01, max_estimated_usd: 0.5, timeout_ms: 10000,
|
|
10
|
+
profiles: Object.freeze(builtInProfiles().map(profile => Object.freeze(profile))),
|
|
11
|
+
profile_descriptions: Object.freeze(defaultCatalog.descriptions),
|
|
12
|
+
});
|
|
13
|
+
const clone = (value) => structuredClone(value);
|
|
14
|
+
const canonical = (value) => Array.isArray(value) ? value.map(canonical) : isRecord(value) ? Object.fromEntries(Object.keys(value).sort().map(k => [k, canonical(value[k])])) : value;
|
|
15
|
+
const digest = (value) => {
|
|
16
|
+
const serialized = JSON.stringify(canonical(value));
|
|
17
|
+
if (serialized === undefined)
|
|
18
|
+
throw new Error('invalid_digest');
|
|
19
|
+
return createHash('sha256').update(serialized).digest('hex');
|
|
20
|
+
};
|
|
21
|
+
const empty = () => ({ version: 1, calls: 0, estimated_usd: 0, days: {}, requests: {} });
|
|
22
|
+
const own = (obj, key) => Object.hasOwn(obj, key);
|
|
23
|
+
const keysOnly = (obj, allowed) => isRecord(obj) && Object.keys(obj).every(k => allowed.includes(k));
|
|
24
|
+
const safeInt = (n, max = 1e9) => typeof n === 'number' && Number.isSafeInteger(n) && n >= 0 && n <= max;
|
|
25
|
+
const safeVersion = (value) => typeof value === 'string' && /^[A-Za-z0-9_.-]{1,64}$/.test(value);
|
|
26
|
+
const usageOf = (usage) => {
|
|
27
|
+
if (!keysOnly(usage, ['input_tokens', 'output_tokens', 'total_tokens']))
|
|
28
|
+
throw new Error('invalid_usage');
|
|
29
|
+
const { input_tokens, output_tokens, total_tokens } = usage;
|
|
30
|
+
if (!safeInt(input_tokens) || !safeInt(output_tokens) || !safeInt(total_tokens) || total_tokens !== input_tokens + output_tokens)
|
|
31
|
+
throw new Error('invalid_usage');
|
|
32
|
+
return { input_tokens, output_tokens, total_tokens };
|
|
33
|
+
};
|
|
34
|
+
// Runtime eligibility stays within the current finite catalog; archival compatibility below does not expand it.
|
|
35
|
+
const supportedProfile = (p) => isSupportedProfile(p);
|
|
36
|
+
// Storage compatibility only. These retired tuples may be read from an old
|
|
37
|
+
// ledger, but do not enter the runtime catalog or grant Hook execution rights.
|
|
38
|
+
const legacyStoredProfiles = [
|
|
39
|
+
{ id: 'codex-routine', client: 'codex', model: 'gpt-6.1-sol', reasoning_effort: 'low' },
|
|
40
|
+
{ id: 'codex-deep', client: 'codex', model: 'gpt-6-astra', reasoning_effort: 'high' },
|
|
41
|
+
{ id: 'claude-routine', client: 'claude', model: 'haiku' },
|
|
42
|
+
{ id: 'claude-deep', client: 'claude', model: 'sonnet' },
|
|
43
|
+
];
|
|
44
|
+
function isStoredProfile(value) {
|
|
45
|
+
if (isSupportedProfile(value))
|
|
46
|
+
return true;
|
|
47
|
+
if (!isRecord(value) || Object.getOwnPropertySymbols(value).length)
|
|
48
|
+
return false;
|
|
49
|
+
const descriptors = Object.getOwnPropertyDescriptors(value);
|
|
50
|
+
return legacyStoredProfiles.some(profile => {
|
|
51
|
+
const expected = Object.entries(profile);
|
|
52
|
+
return Object.keys(descriptors).length === expected.length && expected.every(([key, field]) => own(descriptors, key) && own(descriptors[key], 'value') && descriptors[key].enumerable && descriptors[key].value === field);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const outcomeReasons = {
|
|
56
|
+
recommended: ['allowed_profile'],
|
|
57
|
+
keep_current: ['preserved_constraints', 'unsupported_profile', 'invalid_response', 'invalid_choice', 'provider_abstained', 'no_candidates'],
|
|
58
|
+
needs_context: ['insufficient_context', 'provider_needs_context'],
|
|
59
|
+
unavailable: ['budget_exhausted'],
|
|
60
|
+
};
|
|
61
|
+
const isClient = (value) => value === 'codex' || value === 'claude';
|
|
62
|
+
const preservedFields = ['model_pinned', 'effort_pinned', 'role_pinned', 'fork_inherits'];
|
|
63
|
+
export class Router {
|
|
64
|
+
policy;
|
|
65
|
+
provider;
|
|
66
|
+
ledgerPath;
|
|
67
|
+
state;
|
|
68
|
+
active;
|
|
69
|
+
constructor({ policy = defaultPolicy, provider, ledgerPath } = {}) {
|
|
70
|
+
this.policy = clone(policy);
|
|
71
|
+
const integerCaps = { max_summary_bytes: 64000, max_concurrency: 16, max_daily_calls: 100000, max_total_calls: 1000000, timeout_ms: 60000 };
|
|
72
|
+
if (!safeVersion(policy.version) || !safeVersion(policy.catalog_version))
|
|
73
|
+
throw new Error('invalid_policy');
|
|
74
|
+
for (const k of Object.keys(integerCaps)) {
|
|
75
|
+
if (!safeInt(this.policy[k], integerCaps[k]) || this.policy[k] === 0)
|
|
76
|
+
throw new Error('invalid_policy');
|
|
77
|
+
}
|
|
78
|
+
for (const k of ['estimated_call_usd', 'max_estimated_usd'])
|
|
79
|
+
if (!Number.isFinite(policy[k]) || policy[k] <= 0 || policy[k] > 100)
|
|
80
|
+
throw new Error('invalid_policy');
|
|
81
|
+
if (typeof provider !== 'function')
|
|
82
|
+
throw new Error('invalid_provider');
|
|
83
|
+
if (!Array.isArray(policy.profiles) || policy.profiles.length > 20 || policy.profiles.some(p => !p || typeof p !== 'object') || new Set(policy.profiles.map(p => p.id)).size !== policy.profiles.length)
|
|
84
|
+
throw new Error('invalid_policy');
|
|
85
|
+
for (const p of policy.profiles) {
|
|
86
|
+
if (!keysOnly(p, ['id', 'client', 'model', 'reasoning_effort']) || typeof p.id !== 'string' || !/^[a-z0-9-]{1,64}$/.test(p.id) || !supportedProfile(p) || (p.client === 'claude' && own(p, 'reasoning_effort')))
|
|
87
|
+
throw new Error('invalid_policy');
|
|
88
|
+
}
|
|
89
|
+
if (policy.profile_descriptions !== undefined && (!isRecord(policy.profile_descriptions) || Object.entries(policy.profile_descriptions).some(([id, value]) => !/^[a-z0-9-]{1,64}$/.test(id) || typeof value !== 'string' || !value.trim() || Buffer.byteLength(value) > 2400)))
|
|
90
|
+
throw new Error('invalid_policy');
|
|
91
|
+
this.provider = provider;
|
|
92
|
+
this.ledgerPath = ledgerPath;
|
|
93
|
+
this.state = empty();
|
|
94
|
+
this.active = 0;
|
|
95
|
+
}
|
|
96
|
+
capabilities() {
|
|
97
|
+
return { policy_version: this.policy.version, catalog_version: this.policy.catalog_version,
|
|
98
|
+
profiles: clone(this.policy.profiles), profile_descriptions: clone(this.policy.profile_descriptions ?? {}), application: 'advisory_only',
|
|
99
|
+
storage: this.ledgerPath ? 'persistent_metadata' : 'in_memory_fixture_only',
|
|
100
|
+
cost_guard: 'local_estimate_not_billing_cap', limits: Object.fromEntries(Object.entries(this.policy).filter(([k]) => k.startsWith('max_') || ['estimated_call_usd', 'timeout_ms'].includes(k))) };
|
|
101
|
+
}
|
|
102
|
+
result(status, reason, extra = {}) {
|
|
103
|
+
return { status, reason, application: 'advisory_only', policy_version: this.policy.version, catalog_version: this.policy.catalog_version, ...extra };
|
|
104
|
+
}
|
|
105
|
+
validateOutcome(result) {
|
|
106
|
+
if (!keysOnly(result, ['status', 'reason', 'application', 'policy_version', 'catalog_version', 'profile_id', 'profile', 'usage', 'latency_ms', 'confidence']) ||
|
|
107
|
+
typeof result.status !== 'string' || !own(outcomeReasons, result.status) || typeof result.reason !== 'string' || !outcomeReasons[result.status].includes(result.reason) || result.application !== 'advisory_only' ||
|
|
108
|
+
!safeVersion(result.policy_version) || !safeVersion(result.catalog_version) ||
|
|
109
|
+
(own(result, 'latency_ms') && (typeof result.latency_ms !== 'number' || !Number.isFinite(result.latency_ms) || result.latency_ms < 0 || result.latency_ms > 3600000)) ||
|
|
110
|
+
(own(result, 'confidence') && (typeof result.confidence !== 'number' || !Number.isFinite(result.confidence) || result.confidence < 0 || result.confidence > 1)))
|
|
111
|
+
throw new Error('invalid_outcome');
|
|
112
|
+
if (own(result, 'usage'))
|
|
113
|
+
usageOf(result.usage);
|
|
114
|
+
if (result.status === 'recommended') {
|
|
115
|
+
// Validate archival shape/known provenance without requiring membership
|
|
116
|
+
// in today's selected subset. Eligibility is checked again before reuse.
|
|
117
|
+
if (!isStoredProfile(result.profile) || result.profile_id !== result.profile.id)
|
|
118
|
+
throw new Error('invalid_outcome');
|
|
119
|
+
}
|
|
120
|
+
else if (own(result, 'profile') || own(result, 'profile_id'))
|
|
121
|
+
throw new Error('invalid_outcome');
|
|
122
|
+
}
|
|
123
|
+
load() {
|
|
124
|
+
if (!this.ledgerPath)
|
|
125
|
+
return;
|
|
126
|
+
try {
|
|
127
|
+
const stat = lstatSync(this.ledgerPath);
|
|
128
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
129
|
+
throw new Error('unsafe_ledger');
|
|
130
|
+
chmodSync(this.ledgerPath, 0o600);
|
|
131
|
+
const state = JSON.parse(readFileSync(this.ledgerPath, 'utf8'));
|
|
132
|
+
if (!keysOnly(state, ['version', 'calls', 'estimated_usd', 'days', 'requests']) || state.version !== 1 || !safeInt(state.calls) || typeof state.estimated_usd !== 'number' || !Number.isFinite(state.estimated_usd) || state.estimated_usd < 0 ||
|
|
133
|
+
!isRecord(state.days) || !isRecord(state.requests))
|
|
134
|
+
throw new Error('invalid_ledger');
|
|
135
|
+
const days = state.days;
|
|
136
|
+
if (Object.entries(days).some(([day, calls]) => !/^\d{4}-\d{2}-\d{2}$/.test(day) || !safeInt(calls)))
|
|
137
|
+
throw new Error('invalid_ledger');
|
|
138
|
+
const validatedDays = days;
|
|
139
|
+
if (Object.values(validatedDays).reduce((a, b) => a + b, 0) !== state.calls)
|
|
140
|
+
throw new Error('invalid_ledger');
|
|
141
|
+
for (const [id, record] of Object.entries(state.requests)) {
|
|
142
|
+
if (!/^[A-Za-z0-9_-]{1,96}$/.test(id) || ['__proto__', 'constructor', 'prototype'].includes(id) || !keysOnly(record, ['digest', 'state', 'result']) || typeof record.digest !== 'string' || !/^[a-f0-9]{64}$/.test(record.digest) || (record.state !== 'complete' && record.state !== 'unknown'))
|
|
143
|
+
throw new Error('invalid_ledger');
|
|
144
|
+
if (record.state === 'complete')
|
|
145
|
+
this.validateOutcome(record.result);
|
|
146
|
+
else if (own(record, 'result'))
|
|
147
|
+
throw new Error('invalid_ledger');
|
|
148
|
+
}
|
|
149
|
+
// All records, counters and outcomes were validated above; no parsed
|
|
150
|
+
// JSON shape is trusted at the file boundary before those checks.
|
|
151
|
+
this.state = state;
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
if (errorCode(e) !== 'ENOENT')
|
|
155
|
+
throw e;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
save() {
|
|
159
|
+
if (!this.ledgerPath)
|
|
160
|
+
return;
|
|
161
|
+
const tmp = `${this.ledgerPath}.write-${process.pid}`;
|
|
162
|
+
let fd;
|
|
163
|
+
try {
|
|
164
|
+
fd = openSync(tmp, 'wx', 0o600);
|
|
165
|
+
writeFileSync(fd, JSON.stringify(this.state));
|
|
166
|
+
closeSync(fd);
|
|
167
|
+
fd = undefined;
|
|
168
|
+
renameSync(tmp, this.ledgerPath);
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
if (fd !== undefined)
|
|
172
|
+
closeSync(fd);
|
|
173
|
+
try {
|
|
174
|
+
unlinkSync(tmp);
|
|
175
|
+
}
|
|
176
|
+
catch (e) {
|
|
177
|
+
if (errorCode(e) !== 'ENOENT')
|
|
178
|
+
throw e;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/** Strict native-routing path: preserves existing host-evidence and digest semantics. */
|
|
183
|
+
async recommend(input) {
|
|
184
|
+
return this.route(input, 'hook');
|
|
185
|
+
}
|
|
186
|
+
/** Independent task advice. Omitted constraints mean no caller-declared pins,
|
|
187
|
+
* not verified host settings. A legacy provenance flag grants no authority. */
|
|
188
|
+
async advise(input) {
|
|
189
|
+
const invalid = () => this.result('unavailable', 'invalid_input');
|
|
190
|
+
if (!keysOnly(input, ['request_id', 'client', 'task', 'eligible_profile_ids', 'constraints']) || !isClient(input.client))
|
|
191
|
+
return invalid();
|
|
192
|
+
const constraints = own(input, 'constraints') ? input.constraints : {};
|
|
193
|
+
if (!keysOnly(constraints, [...preservedFields, 'provenance_verified']) || Object.values(constraints).some(value => typeof value !== 'boolean'))
|
|
194
|
+
return invalid();
|
|
195
|
+
const declared = Object.fromEntries(preservedFields.map(key => [key, constraints[key] ?? false]));
|
|
196
|
+
return this.route({ ...input,
|
|
197
|
+
eligible_profile_ids: own(input, 'eligible_profile_ids') ? input.eligible_profile_ids : this.policy.profiles.filter(profile => profile.client === input.client).map(profile => profile.id),
|
|
198
|
+
constraints: declared,
|
|
199
|
+
}, 'advisory');
|
|
200
|
+
}
|
|
201
|
+
async route(input, kind) {
|
|
202
|
+
const invalid = () => this.result('unavailable', 'invalid_input');
|
|
203
|
+
if (!keysOnly(input, ['request_id', 'client', 'task', 'eligible_profile_ids', 'constraints']) ||
|
|
204
|
+
typeof input.request_id !== 'string' || !/^[A-Za-z0-9_-]{1,96}$/.test(input.request_id) || !isClient(input.client) ||
|
|
205
|
+
['__proto__', 'constructor', 'prototype'].includes(input.request_id) ||
|
|
206
|
+
!keysOnly(input.task, ['summary', 'operation', 'context_sufficient']) || typeof input.task.summary !== 'string' ||
|
|
207
|
+
!input.task.summary.trim() || Buffer.byteLength(input.task.summary) > this.policy.max_summary_bytes ||
|
|
208
|
+
typeof input.task.operation !== 'string' || !['read_only', 'write', 'review', 'analysis'].includes(input.task.operation) || typeof input.task.context_sufficient !== 'boolean' ||
|
|
209
|
+
!Array.isArray(input.eligible_profile_ids) || (!input.eligible_profile_ids.length && this.policy.profiles.some(p => p.client === input.client)) || input.eligible_profile_ids.length > 20 ||
|
|
210
|
+
input.eligible_profile_ids.some((id) => typeof id !== 'string') || new Set(input.eligible_profile_ids).size !== input.eligible_profile_ids.length ||
|
|
211
|
+
!keysOnly(input.constraints, ['model_pinned', 'effort_pinned', 'role_pinned', 'fork_inherits', 'provenance_verified']) ||
|
|
212
|
+
Object.values(input.constraints).some(v => typeof v !== 'boolean'))
|
|
213
|
+
return invalid();
|
|
214
|
+
if (/\bsk-[A-Za-z0-9_-]{12,}|\bBearer\s+[A-Za-z0-9._~+\/-]{8,}={0,2}/i.test(input.task.summary))
|
|
215
|
+
return this.result('keep_current', 'sensitive_input');
|
|
216
|
+
const req = clone(input);
|
|
217
|
+
const profiles = this.policy.profiles.filter(p => p.client === req.client && req.eligible_profile_ids.includes(p.id));
|
|
218
|
+
if (!profiles.length && !req.eligible_profile_ids.length)
|
|
219
|
+
return this.result('keep_current', 'no_candidates');
|
|
220
|
+
if (profiles.length !== req.eligible_profile_ids.length)
|
|
221
|
+
return this.result('keep_current', 'unsupported_profile');
|
|
222
|
+
// Keep historical strict-path hashes unchanged. Advice has a separate digest
|
|
223
|
+
// namespace within the SAME request ledger, so cross-purpose ID reuse cannot
|
|
224
|
+
// reuse results or automatically retry a previously uncertain paid request.
|
|
225
|
+
const hash = kind === 'hook' ? digest({ input: req, policy: this.policy }) : digest({ kind: 'advisory-v1', input: req, policy: this.policy });
|
|
226
|
+
if (this.active >= this.policy.max_concurrency)
|
|
227
|
+
return this.result('unavailable', 'concurrency_limit');
|
|
228
|
+
let lock;
|
|
229
|
+
if (this.ledgerPath) {
|
|
230
|
+
try {
|
|
231
|
+
lock = openSync(`${this.ledgerPath}.lock`, 'wx', 0o600);
|
|
232
|
+
}
|
|
233
|
+
catch {
|
|
234
|
+
return this.result('unavailable', 'ledger_locked');
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
this.active++;
|
|
238
|
+
try {
|
|
239
|
+
this.load();
|
|
240
|
+
const previous = own(this.state.requests, req.request_id) ? this.state.requests[req.request_id] : undefined;
|
|
241
|
+
if (previous) {
|
|
242
|
+
if (previous.digest !== hash)
|
|
243
|
+
return this.result('unavailable', 'request_conflict');
|
|
244
|
+
if (previous.state !== 'complete')
|
|
245
|
+
return this.result('unavailable', 'outcome_unknown');
|
|
246
|
+
this.validateOutcome(previous.result);
|
|
247
|
+
if (previous.result.policy_version !== this.policy.version || previous.result.catalog_version !== this.policy.catalog_version ||
|
|
248
|
+
(previous.result.status === 'recommended' && !profiles.some(p => p.id === previous.result?.profile_id && digest(p) === digest(previous.result?.profile))))
|
|
249
|
+
return this.result('unavailable', 'ledger_error');
|
|
250
|
+
return { ...clone(previous.result), cached: true };
|
|
251
|
+
}
|
|
252
|
+
const complete = (result) => {
|
|
253
|
+
this.state.requests[req.request_id] = { digest: hash, state: 'complete', result };
|
|
254
|
+
this.save();
|
|
255
|
+
return clone(result);
|
|
256
|
+
};
|
|
257
|
+
if ((kind === 'hook' && req.constraints.provenance_verified !== true) || preservedFields.some(k => req.constraints[k] !== false))
|
|
258
|
+
return complete(this.result('keep_current', 'preserved_constraints'));
|
|
259
|
+
if (!req.task.context_sufficient)
|
|
260
|
+
return complete(this.result('needs_context', 'insufficient_context'));
|
|
261
|
+
const day = new Date().toISOString().slice(0, 10);
|
|
262
|
+
if (this.state.calls >= this.policy.max_total_calls || (this.state.days[day] ?? 0) >= this.policy.max_daily_calls || this.state.estimated_usd + this.policy.estimated_call_usd > this.policy.max_estimated_usd + 1e-12)
|
|
263
|
+
return complete(this.result('unavailable', 'budget_exhausted'));
|
|
264
|
+
this.state.calls++;
|
|
265
|
+
this.state.days[day] = (this.state.days[day] ?? 0) + 1;
|
|
266
|
+
this.state.estimated_usd += this.policy.estimated_call_usd;
|
|
267
|
+
this.state.requests[req.request_id] = { digest: hash, state: 'unknown' };
|
|
268
|
+
this.save();
|
|
269
|
+
const controller = new AbortController();
|
|
270
|
+
let timer;
|
|
271
|
+
const started = performance.now();
|
|
272
|
+
const latency = () => Math.round((performance.now() - started) * 1000) / 1000;
|
|
273
|
+
try {
|
|
274
|
+
const response = await Promise.race([
|
|
275
|
+
Promise.resolve().then(() => this.provider({ input: clone(req), profiles: clone(profiles), signal: controller.signal })),
|
|
276
|
+
new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(Object.assign(new Error('timeout'), { code: 'timeout' })); }, this.policy.timeout_ms); }),
|
|
277
|
+
]);
|
|
278
|
+
if (!isRecord(response) || typeof response.choice !== 'string' || (own(response, 'confidence') && (typeof response.confidence !== 'number' || !Number.isFinite(response.confidence) || response.confidence < 0 || response.confidence > 1)))
|
|
279
|
+
return complete(this.result('keep_current', 'invalid_response', { latency_ms: latency() }));
|
|
280
|
+
const metadata = { latency_ms: latency() };
|
|
281
|
+
if (typeof response.confidence === 'number')
|
|
282
|
+
metadata.confidence = response.confidence;
|
|
283
|
+
if (own(response, 'usage')) {
|
|
284
|
+
try {
|
|
285
|
+
metadata.usage = usageOf(response.usage);
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return complete(this.result('keep_current', 'invalid_response', { latency_ms: latency() }));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (response.choice === 'keep_current')
|
|
292
|
+
return complete(this.result('keep_current', 'provider_abstained', metadata));
|
|
293
|
+
if (response.choice === 'needs_context')
|
|
294
|
+
return complete(this.result('needs_context', 'provider_needs_context', metadata));
|
|
295
|
+
const profile = profiles.find(p => p.id === response.choice);
|
|
296
|
+
if (!profile)
|
|
297
|
+
return complete(this.result('keep_current', 'invalid_choice', metadata));
|
|
298
|
+
return complete(this.result('recommended', 'allowed_profile', { profile_id: profile.id, profile: clone(profile), ...metadata }));
|
|
299
|
+
}
|
|
300
|
+
catch (e) {
|
|
301
|
+
// A failed transport can have consumed a paid call. Preserve its unknown marker.
|
|
302
|
+
const allowed = ['timeout', 'refused', 'truncated', 'invalid_json', 'unauthorized', 'rate_limited', 'network'];
|
|
303
|
+
const code = errorCode(e);
|
|
304
|
+
return this.result('unavailable', code !== undefined && allowed.includes(code) ? code : 'provider_error', { latency_ms: latency() });
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
clearTimeout(timer);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return this.result('unavailable', 'ledger_error');
|
|
312
|
+
}
|
|
313
|
+
finally {
|
|
314
|
+
this.active--;
|
|
315
|
+
if (lock !== undefined) {
|
|
316
|
+
closeSync(lock);
|
|
317
|
+
try {
|
|
318
|
+
unlinkSync(`${this.ledgerPath}.lock`);
|
|
319
|
+
}
|
|
320
|
+
catch { }
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Diagnostic } from './report.ts';
|
|
2
|
+
export interface DiagnosticOptions {
|
|
3
|
+
env?: unknown;
|
|
4
|
+
node?: unknown;
|
|
5
|
+
platform?: unknown;
|
|
6
|
+
arch?: unknown;
|
|
7
|
+
now?: Date;
|
|
8
|
+
reportId?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Read-only local checks. No hosts are started and no API authentication probe is made. */
|
|
11
|
+
export declare function collectDiagnostics({ env, node, platform, arch, now, reportId }?: DiagnosticOptions): Diagnostic;
|