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.
Files changed (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +45 -0
  3. package/README.ja.md +45 -0
  4. package/README.md +45 -0
  5. package/README.zh-CN.md +45 -0
  6. package/dist/bin/aptly.d.ts +2 -0
  7. package/dist/bin/aptly.js +122 -0
  8. package/dist/src/automatic-hook.d.ts +42 -0
  9. package/dist/src/automatic-hook.js +232 -0
  10. package/dist/src/catalog.d.ts +19 -0
  11. package/dist/src/catalog.js +135 -0
  12. package/dist/src/config.d.ts +24 -0
  13. package/dist/src/config.js +75 -0
  14. package/dist/src/core.d.ts +50 -0
  15. package/dist/src/core.js +324 -0
  16. package/dist/src/diagnostics.d.ts +11 -0
  17. package/dist/src/diagnostics.js +68 -0
  18. package/dist/src/files.d.ts +10 -0
  19. package/dist/src/files.js +70 -0
  20. package/dist/src/hook.d.ts +20 -0
  21. package/dist/src/hook.js +102 -0
  22. package/dist/src/inspect.d.ts +31 -0
  23. package/dist/src/inspect.js +219 -0
  24. package/dist/src/model-info.d.ts +32 -0
  25. package/dist/src/model-info.js +223 -0
  26. package/dist/src/provenance.d.ts +62 -0
  27. package/dist/src/provenance.js +266 -0
  28. package/dist/src/provider.d.ts +9 -0
  29. package/dist/src/provider.js +80 -0
  30. package/dist/src/report.d.ts +51 -0
  31. package/dist/src/report.js +177 -0
  32. package/dist/src/runtime.d.ts +8 -0
  33. package/dist/src/runtime.js +22 -0
  34. package/dist/src/server.d.ts +7 -0
  35. package/dist/src/server.js +80 -0
  36. package/dist/src/setup.d.ts +22 -0
  37. package/dist/src/setup.js +408 -0
  38. package/dist/src/types.d.ts +76 -0
  39. package/dist/src/types.js +2 -0
  40. package/dist/src/version.d.ts +1 -0
  41. package/dist/src/version.js +1 -0
  42. package/docs/ADVISORY_USAGE.md +196 -0
  43. package/docs/DRAFT_0_1_0.md +55 -0
  44. package/docs/guide/en.md +190 -0
  45. package/docs/guide/ja.md +190 -0
  46. package/docs/guide/ko.md +190 -0
  47. package/docs/guide/zh-CN.md +190 -0
  48. package/examples/claude-mcp.json +15 -0
  49. package/examples/codex-mcp.toml +9 -0
  50. package/examples/recommend.claude.json +9 -0
  51. package/examples/recommend.codex.json +9 -0
  52. package/examples/router.shadow.json +5 -0
  53. package/examples/settings-inventory.json +5 -0
  54. package/package.json +75 -0
@@ -0,0 +1,68 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { VERSION } from './version.js';
3
+ import { loadConfiguration, loadHostContext } from './config.js';
4
+ import { assertPrivateStatePath } from './files.js';
5
+ const platforms = new Set(['aix', 'darwin', 'freebsd', 'linux', 'openbsd', 'sunos', 'win32', 'android']);
6
+ const architectures = new Set(['arm', 'arm64', 'ia32', 'loong64', 'mips', 'mipsel', 'ppc', 'ppc64', 'riscv64', 's390', 's390x', 'x64']);
7
+ const envValue = (env, name) => {
8
+ try {
9
+ if (env === null || (typeof env !== 'object' && typeof env !== 'function'))
10
+ return undefined;
11
+ const descriptor = Object.getOwnPropertyDescriptor(env, name);
12
+ return descriptor && Object.hasOwn(descriptor, 'value') && typeof descriptor.value === 'string' ? descriptor.value : undefined;
13
+ }
14
+ catch {
15
+ return undefined;
16
+ }
17
+ };
18
+ /** Read-only local checks. No hosts are started and no API authentication probe is made. */
19
+ export function collectDiagnostics({ env = process.env, node = process.versions.node, platform = process.platform, arch = process.arch, now = new Date(), reportId = randomUUID() } = {}) {
20
+ const checks = [];
21
+ const add = (id, status, code) => { checks.push({ id, status, code }); };
22
+ const version = typeof node === 'string' && /^\d{1,4}\.\d{1,4}\.\d{1,4}$/.test(node) ? node : '0.0.0';
23
+ add('node_runtime', Number(version.split('.')[0]) >= 24 ? 'ok' : 'error', Number(version.split('.')[0]) >= 24 ? 'node_supported' : 'node_unsupported');
24
+ const rawProvider = envValue(env, 'ROUTER_PROVIDER');
25
+ const provider = rawProvider === undefined ? 'fixture' : rawProvider;
26
+ add('classification_provider', ['fixture', 'decisions'].includes(provider) ? 'ok' : 'error', provider === 'fixture' ? 'provider_fixture' : provider === 'decisions' ? 'provider_decisions' : 'provider_invalid');
27
+ if (provider === 'fixture')
28
+ add('api_key', 'ok', 'key_not_required');
29
+ else {
30
+ const key = envValue(env, 'OPENAI_API_KEY');
31
+ const present = typeof key === 'string' && key.trim().length > 0;
32
+ add('api_key', present ? 'unknown' : 'error', present ? 'key_present_unverified' : 'key_missing');
33
+ }
34
+ const ledger = envValue(env, 'ROUTER_LEDGER_PATH');
35
+ if (!ledger)
36
+ add('ledger_location', provider === 'decisions' ? 'error' : 'unknown', 'ledger_not_configured');
37
+ else {
38
+ try {
39
+ assertPrivateStatePath(ledger);
40
+ add('ledger_location', 'ok', 'ledger_private_location_ready');
41
+ }
42
+ catch {
43
+ add('ledger_location', 'error', 'ledger_location_unavailable');
44
+ }
45
+ }
46
+ // Configuration and evidence contents are interpreted locally; never copied
47
+ // into the report. Paths, values, session identities and digests stay private.
48
+ const configuredPath = envValue(env, 'ROUTER_CONFIG_PATH');
49
+ const config = loadConfiguration({ ROUTER_CONFIG_PATH: configuredPath });
50
+ if (config.error)
51
+ add('operator_configuration', 'error', config.error === 'configuration_invalid' ? 'configuration_invalid' : 'configuration_unavailable');
52
+ else
53
+ add('operator_configuration', 'ok', configuredPath ? 'configuration_valid' : 'configuration_default_shadow');
54
+ if (!config.context_path)
55
+ add('host_evidence', 'unknown', 'host_evidence_not_configured');
56
+ else {
57
+ const loaded = loadHostContext(config.context_path);
58
+ const code = loaded.reason === 'host_evidence_loaded' ? 'host_evidence_loaded_unverified' : loaded.reason === 'context_sources_changed' ? 'host_evidence_changed' : 'host_evidence_unavailable';
59
+ add('host_evidence', code === 'host_evidence_loaded_unverified' ? 'unknown' : 'warning', code);
60
+ }
61
+ add('host_cli_versions', 'unknown', 'host_cli_not_probed');
62
+ const summary = { ok: 0, warning: 0, error: 0, unknown: 0 };
63
+ for (const check of checks)
64
+ summary[check.status]++;
65
+ return { schema_version: 1, report_id: reportId, generated_at: now.toISOString(), tool: { name: 'aptly', version: VERSION },
66
+ environment: { node: version, platform: typeof platform === 'string' && platforms.has(platform) ? platform : 'unknown', arch: typeof arch === 'string' && architectures.has(arch) ? arch : 'unknown' }, checks, summary,
67
+ privacy: { task_text: false, paths: false, environment_values: false, credentials: false, raw_errors: false }, network_requests: 0, paid_calls: 0 };
68
+ }
@@ -0,0 +1,10 @@
1
+ export declare function systemAliasAllowed(path: string, stat: {
2
+ uid: number;
3
+ }, target: string, platform?: string): boolean;
4
+ export declare function assertNoRedirectedAncestors(path: unknown): asserts path is string;
5
+ export declare function assertPrivateStatePath(path: unknown): asserts path is string;
6
+ /** Bounded nonblocking read; no raw path or parser message is part of its error. */
7
+ export declare function readRegularFile(path: unknown, { privateFile, maxBytes }?: {
8
+ privateFile?: boolean;
9
+ maxBytes?: number;
10
+ }): Buffer;
@@ -0,0 +1,70 @@
1
+ import { lstatSync, readlinkSync, openSync, fstatSync, readSync, closeSync, constants } from 'node:fs';
2
+ import { dirname, isAbsolute, resolve } from 'node:path';
3
+ import { errorCode } from './types.js';
4
+ // macOS root-owned compatibility aliases are part of the OS filesystem layout,
5
+ // not operator/config redirects. Arbitrary user-owned symlinks stay forbidden.
6
+ export function systemAliasAllowed(path, stat, target, platform = process.platform) {
7
+ const aliases = { '/tmp': 'private/tmp', '/var': 'private/var', '/etc': 'private/etc' };
8
+ return platform === 'darwin' && stat.uid === 0 && Object.hasOwn(aliases, path) && aliases[path] === target;
9
+ }
10
+ export function assertNoRedirectedAncestors(path) {
11
+ if (typeof path !== 'string' || !isAbsolute(path))
12
+ throw new Error('unsafe_path');
13
+ let current = resolve(path);
14
+ while (true) {
15
+ const stat = lstatSync(current);
16
+ if (stat.isSymbolicLink() && !systemAliasAllowed(current, stat, readlinkSync(current)))
17
+ throw new Error('unsafe_path');
18
+ const parent = dirname(current);
19
+ if (parent === current)
20
+ break;
21
+ current = parent;
22
+ }
23
+ }
24
+ export function assertPrivateStatePath(path) {
25
+ if (typeof path !== 'string' || !isAbsolute(path))
26
+ throw new Error('private_state_path_required');
27
+ assertNoRedirectedAncestors(dirname(path));
28
+ const parent = lstatSync(dirname(path));
29
+ if (!parent.isDirectory() || (parent.mode & 0o077) || (process.getuid && parent.uid !== process.getuid()))
30
+ throw new Error('private_state_directory_required');
31
+ try {
32
+ const file = lstatSync(path);
33
+ if (!file.isFile() || file.isSymbolicLink() || (process.getuid && file.uid !== process.getuid()))
34
+ throw new Error('private_state_file_required');
35
+ }
36
+ catch (error) {
37
+ if (errorCode(error) !== 'ENOENT')
38
+ throw error;
39
+ }
40
+ }
41
+ /** Bounded nonblocking read; no raw path or parser message is part of its error. */
42
+ export function readRegularFile(path, { privateFile = false, maxBytes = 65536 } = {}) {
43
+ let fd;
44
+ try {
45
+ assertNoRedirectedAncestors(path);
46
+ const before = lstatSync(path);
47
+ if (!before.isFile() || before.isSymbolicLink() || before.size > maxBytes)
48
+ throw new Error('unsafe_file');
49
+ fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
50
+ const stat = fstatSync(fd);
51
+ if (!stat.isFile() || stat.dev !== before.dev || stat.ino !== before.ino || stat.size > maxBytes ||
52
+ (privateFile && ((stat.mode & 0o077) || (process.getuid && stat.uid !== process.getuid()))))
53
+ throw new Error('unsafe_file');
54
+ const buffer = Buffer.alloc(maxBytes + 1);
55
+ let size = 0;
56
+ while (size < buffer.length) {
57
+ const count = readSync(fd, buffer, size, buffer.length - size, null);
58
+ if (!count)
59
+ break;
60
+ size += count;
61
+ }
62
+ if (size > maxBytes)
63
+ throw new Error('unsafe_file');
64
+ return buffer.subarray(0, size);
65
+ }
66
+ finally {
67
+ if (fd !== undefined)
68
+ closeSync(fd);
69
+ }
70
+ }
@@ -0,0 +1,20 @@
1
+ import type { UnknownRecord, RoutingInput, RoutingPolicy } from './types.ts';
2
+ export interface HookOptions {
3
+ client?: unknown;
4
+ event?: unknown;
5
+ recommendation?: unknown;
6
+ mode?: string;
7
+ compatibilityVerified?: boolean;
8
+ policy?: RoutingPolicy;
9
+ }
10
+ export interface HookOutput {
11
+ hookSpecificOutput?: {
12
+ hookEventName: 'PreToolUse';
13
+ permissionDecision?: 'allow';
14
+ updatedInput: UnknownRecord;
15
+ };
16
+ }
17
+ /** Extract only a task label and safe structural features, never prompt/message/transcript. */
18
+ export declare function hookInput({ client, event, policy }?: HookOptions): RoutingInput | null;
19
+ /** Native Hook JSON, defaulting to no permission or argument intervention. */
20
+ export declare function hookOutput({ client, event, recommendation, mode, compatibilityVerified, policy }?: HookOptions): HookOutput;
@@ -0,0 +1,102 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { defaultPolicy } from './core.js';
3
+ import { isSupportedProfile } from './catalog.js';
4
+ const own = (value, key) => Object.hasOwn(value, key);
5
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) &&
6
+ [Object.prototype, null].includes(Object.getPrototypeOf(value));
7
+ function target(client, event) {
8
+ if ((client !== 'codex' && client !== 'claude') || !object(event) ||
9
+ event.hook_event_name !== 'PreToolUse')
10
+ return null;
11
+ if (!(client === 'codex' ? ['spawn_agent', 'collaborationspawn_agent', 'Agent'] : ['Agent']).includes(typeof event.tool_name === 'string' ? event.tool_name : ''))
12
+ return null;
13
+ const id = event.tool_use_id ?? event.tool_call_id;
14
+ if (typeof id !== 'string' || !id.trim() || id.length > 512)
15
+ return null;
16
+ let input = event.tool_input;
17
+ // Claude 2.1.314 expands an object placeholder as a JSON string, while
18
+ // Codex preserves its JSON type. Decode this bounded transport form only
19
+ // for Claude; neither input nor parse errors are logged or returned.
20
+ if (typeof input === 'string') {
21
+ if (client !== 'claude' || Buffer.byteLength(input) > 16384)
22
+ return null;
23
+ try {
24
+ input = JSON.parse(input);
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ if (!object(input))
31
+ return null;
32
+ return { client, event, input, id };
33
+ }
34
+ function constraints(client, input) {
35
+ return {
36
+ model_pinned: own(input, 'model'),
37
+ effort_pinned: ['effort', 'reasoning_effort', 'model_reasoning_effort'].some(k => own(input, k)),
38
+ role_pinned: ['resume', 'resume_agent', 'agent_id'].some(k => own(input, k)) || (client === 'claude' ? input.subagent_type !== 'general-purpose' :
39
+ own(input, 'agent_type') || own(input, 'role')),
40
+ fork_inherits: client === 'codex' ? input.fork_turns !== 'none' || own(input, 'fork_context') : input.subagent_type === 'fork',
41
+ // Native tool arguments do not prove the source or precedence of settings.
42
+ provenance_verified: false,
43
+ };
44
+ }
45
+ /** Extract only a task label and safe structural features, never prompt/message/transcript. */
46
+ export function hookInput({ client, event, policy = defaultPolicy } = {}) {
47
+ const found = target(client, event);
48
+ if (!found)
49
+ return null;
50
+ const summary = client === 'claude' ? found.input.description : found.input.task_name;
51
+ if (typeof summary !== 'string' || !summary.trim() || Buffer.byteLength(summary) > 4000)
52
+ return null;
53
+ const request_id = createHash('sha256').update(JSON.stringify([
54
+ found.client, found.event.session_id ?? null, found.id,
55
+ ])).digest('hex');
56
+ return {
57
+ request_id, client: found.client,
58
+ task: { summary, operation: 'analysis', context_sufficient: false },
59
+ eligible_profile_ids: policy.profiles.filter(p => p.client === client).map(p => p.id),
60
+ constraints: constraints(found.client, found.input),
61
+ };
62
+ }
63
+ /** Native Hook JSON, defaulting to no permission or argument intervention. */
64
+ export function hookOutput({ client, event, recommendation, mode = 'shadow', compatibilityVerified = false, policy = defaultPolicy } = {}) {
65
+ if (mode !== 'apply' || compatibilityVerified !== true)
66
+ return {};
67
+ const found = target(client, event);
68
+ if (!found || !object(recommendation) || recommendation.status !== 'recommended')
69
+ return {};
70
+ const pins = constraints(found.client, found.input);
71
+ if (['model_pinned', 'effort_pinned', 'role_pinned', 'fork_inherits'].some(k => pins[k]))
72
+ return {};
73
+ const profile = policy.profiles.find(p => p.client === client && p.id === recommendation.profile_id);
74
+ if (!profile || !isSupportedProfile(profile) || recommendation.policy_version !== policy.version ||
75
+ recommendation.catalog_version !== policy.catalog_version)
76
+ return {};
77
+ // Select executable values solely from the trusted catalog. Reject conflicting
78
+ // copies instead of letting a provider supply arbitrary model/effort strings.
79
+ if (own(recommendation, 'model') || own(recommendation, 'reasoning_effort') || own(recommendation, 'effort'))
80
+ return {};
81
+ const suppliedProfile = recommendation.profile;
82
+ if (!object(suppliedProfile) || Object.keys(suppliedProfile).length !== Object.keys(profile).length ||
83
+ Object.entries(profile).some(([key, value]) => suppliedProfile[key] !== value))
84
+ return {};
85
+ let updatedInput;
86
+ try {
87
+ updatedInput = structuredClone(found.input);
88
+ }
89
+ catch {
90
+ return {};
91
+ }
92
+ updatedInput.model = profile.model;
93
+ if (client === 'codex')
94
+ updatedInput.reasoning_effort = profile.reasoning_effort;
95
+ return { hookSpecificOutput: {
96
+ hookEventName: 'PreToolUse',
97
+ // Codex requires allow with updatedInput. Claude permits input-only
98
+ // replacement, retaining the host's normal permission decision path.
99
+ ...(client === 'codex' ? { permissionDecision: 'allow' } : {}),
100
+ updatedInput,
101
+ } };
102
+ }
@@ -0,0 +1,31 @@
1
+ import type { Client } from './types.ts';
2
+ import type { EvidenceState } from './provenance.ts';
3
+ interface SettingStates {
4
+ model: EvidenceState;
5
+ effort: EvidenceState;
6
+ }
7
+ export interface InspectionOptions {
8
+ client?: unknown;
9
+ cwd?: unknown;
10
+ files?: unknown;
11
+ environment?: unknown;
12
+ clientVersion?: unknown;
13
+ }
14
+ export interface InspectionResult {
15
+ client: Client | 'unknown';
16
+ client_version: string;
17
+ layers: Array<SettingStates & {
18
+ source: string;
19
+ }>;
20
+ environment: Record<string, 'set' | 'unset' | 'unknown'>;
21
+ host_extensions: 'unknown';
22
+ evidence_verified: false;
23
+ reason: 'effective_host_context_unobserved';
24
+ }
25
+ /**
26
+ * Explain only explicitly selected files and a caller-supplied env snapshot.
27
+ * No secret values, paths, file bodies or parse errors leave this function.
28
+ * File absence is local evidence about that file, not source completeness.
29
+ */
30
+ export declare function explainSettings({ client, cwd, files, environment, clientVersion }?: InspectionOptions): InspectionResult;
31
+ export {};
@@ -0,0 +1,219 @@
1
+ // Read only explicitly supplied configuration files. This is a redacted local
2
+ // explanation, never evidence of the effective settings of a running host.
3
+ import { constants, closeSync, fstatSync, lstatSync, openSync, readSync } from 'node:fs';
4
+ import { isAbsolute, resolve } from 'node:path';
5
+ import { parse as parseToml } from 'smol-toml';
6
+ import { provenanceRequirements } from './provenance.js';
7
+ import { assertNoRedirectedAncestors } from './files.js';
8
+ import { errorCode } from './types.js';
9
+ const MAX_BYTES = 65536;
10
+ const MAX_FILES = 64;
11
+ const own = (value, key) => Object.hasOwn(value, key);
12
+ const plain = (value) => value !== null && typeof value === 'object' && !Array.isArray(value) &&
13
+ [Object.prototype, null].includes(Object.getPrototypeOf(value));
14
+ const unknown = () => ({ model: 'unknown', effort: 'unknown' });
15
+ const unset = () => ({ model: 'unset', effort: 'unset' });
16
+ const mergeState = (a, b) => a === 'pinned' || b === 'pinned' ? 'pinned' : a === 'unknown' || b === 'unknown' ? 'unknown' : 'unset';
17
+ const merge = (a, b) => ({ model: mergeState(a.model, b.model), effort: mergeState(a.effort, b.effort) });
18
+ const allowedFiles = {
19
+ codex: ['role', 'subagent_defaults', 'profile', 'project', 'user', 'system', 'managed'],
20
+ claude: ['agent', 'settings_flag', 'local', 'project', 'user', 'managed'],
21
+ };
22
+ const MODEL_ENV = new Set(['CODEX_MODEL', 'CLAUDE_CODE_SUBAGENT_MODEL', 'CLAUDE_CODE_SUBAGENT_MODEL_FORCE',
23
+ 'ANTHROPIC_MODEL', 'ANTHROPIC_SMALL_FAST_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL',
24
+ 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_OPUS_MODEL', 'ANTHROPIC_DEFAULT_FABLE_MODEL']);
25
+ const EFFORT_ENV = new Set(['CODEX_REASONING_EFFORT', 'CLAUDE_CODE_EFFORT_LEVEL', 'CLAUDE_CODE_MAX_EFFORT_LEVEL']);
26
+ const CLAUDE_MODEL_POLICIES = ['availableModels', 'deniedModels', 'deniedModelPatterns', 'availableModelsMatch', 'enforceAvailableModels'];
27
+ const CODEX_MODEL_POLICIES = ['allowed_models', 'denied_models', 'model_requirements'];
28
+ function secretPath(path) {
29
+ return path.split(/[\\/]+/).some(part => /^(?:\.env(?:\..*)?|\.ssh|\.aws|\.gnupg|id_rsa|id_ed25519)$/i.test(part) ||
30
+ /(?:^|[._-])(?:auth|oauth|credentials?|secrets?|tokens?)(?:[._-]|$)/i.test(part) || /^Bitwarden CLI$/i.test(part));
31
+ }
32
+ function explicitFile(path) {
33
+ let fd;
34
+ try {
35
+ assertNoRedirectedAncestors(path);
36
+ const before = lstatSync(path);
37
+ if (!before.isFile() || before.size > MAX_BYTES)
38
+ return { status: 'unknown' };
39
+ fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
40
+ const current = fstatSync(fd);
41
+ if (!current.isFile() || current.size > MAX_BYTES || current.dev !== before.dev || current.ino !== before.ino)
42
+ return { status: 'unknown' };
43
+ const buffer = Buffer.alloc(MAX_BYTES + 1);
44
+ let bytes = 0;
45
+ while (bytes < buffer.length) {
46
+ const count = readSync(fd, buffer, bytes, buffer.length - bytes, null);
47
+ if (!count)
48
+ break;
49
+ bytes += count;
50
+ }
51
+ if (bytes > MAX_BYTES)
52
+ return { status: 'unknown' };
53
+ const text = buffer.subarray(0, bytes).toString('utf8');
54
+ if (text.includes('\uFFFD'))
55
+ return { status: 'unknown' };
56
+ return { status: 'read', text };
57
+ }
58
+ catch (error) {
59
+ return { status: errorCode(error) === 'ENOENT' ? 'missing' : 'unknown' };
60
+ }
61
+ finally {
62
+ if (fd !== undefined)
63
+ closeSync(fd);
64
+ }
65
+ }
66
+ // Intentionally a strict frontmatter subset. YAML anchors, lists, multiline
67
+ // scalars, duplicate fields and nested structures stay unknown rather than
68
+ // pretending to resolve a host's full YAML parser behavior.
69
+ function frontmatter(text) {
70
+ const lines = text.replace(/^\uFEFF/, '').split(/\r?\n/);
71
+ if (lines[0] !== '---')
72
+ throw new Error('unknown_frontmatter');
73
+ const end = lines.indexOf('---', 1);
74
+ if (end < 0)
75
+ throw new Error('unknown_frontmatter');
76
+ const data = {};
77
+ const known = new Set(['name', 'description', 'model', 'effort', 'model_reasoning_effort', 'tools', 'disallowedTools',
78
+ 'permissionMode', 'maxTurns', 'memory', 'background', 'isolation', 'color', 'initialPrompt', 'context',
79
+ 'user-invocable', 'disable-model-invocation', 'argument-hint', 'allowed-tools']);
80
+ for (const line of lines.slice(1, end)) {
81
+ if (!line.trim() || line.trimStart().startsWith('#'))
82
+ continue;
83
+ const match = /^([A-Za-z][A-Za-z0-9_-]*):[ \t]*(.*)$/.exec(line);
84
+ if (!match || !known.has(match[1]) || own(data, match[1]))
85
+ throw new Error('unknown_frontmatter');
86
+ const key = match[1], raw = match[2].trim();
87
+ if (!raw || /^(?:[|>&*!\[{]|---$|\.\.\.$)/.test(raw) || /\s[&*!][A-Za-z]/.test(raw))
88
+ throw new Error('unknown_frontmatter');
89
+ if (raw.startsWith('"'))
90
+ data[key] = JSON.parse(raw);
91
+ else if (raw.startsWith("'")) {
92
+ if (!/^'(?:[^']|'')*'$/.test(raw))
93
+ throw new Error('unknown_frontmatter');
94
+ data[key] = raw.slice(1, -1).replace(/''/g, "'");
95
+ }
96
+ else {
97
+ if (/^[\[\]{}]|:\s/.test(raw))
98
+ throw new Error('unknown_frontmatter');
99
+ data[key] = raw.split(/\s+#/)[0].trim();
100
+ }
101
+ if (['model', 'effort', 'model_reasoning_effort'].includes(key) &&
102
+ (typeof data[key] !== 'string' || !data[key] || /^(?:null|~)$/i.test(data[key])))
103
+ throw new Error('unknown_frontmatter');
104
+ }
105
+ return data;
106
+ }
107
+ function present(data, names) { return names.some(key => own(data, key)); }
108
+ function summarize(client, data) {
109
+ if (!plain(data))
110
+ return unknown();
111
+ let result = unset();
112
+ if (own(data, 'model'))
113
+ result.model = 'pinned';
114
+ if (present(data, client === 'codex' ? ['model_reasoning_effort', 'reasoning_effort', 'effort'] : ['effort', 'effortLevel', 'maxEffortLevel']))
115
+ result.effort = 'pinned';
116
+ if (present(data, client === 'codex' ? CODEX_MODEL_POLICIES : CLAUDE_MODEL_POLICIES))
117
+ result.model = 'pinned';
118
+ if (own(data, 'env')) {
119
+ const env = data.env;
120
+ if (!plain(env))
121
+ result = merge(result, unknown());
122
+ else {
123
+ if ([...MODEL_ENV].some(key => own(env, key)))
124
+ result.model = 'pinned';
125
+ if ([...EFFORT_ENV].some(key => own(env, key)))
126
+ result.effort = 'pinned';
127
+ }
128
+ }
129
+ if (client === 'codex' && own(data, 'agents')) {
130
+ if (!plain(data.agents))
131
+ return merge(result, unknown());
132
+ const agents = data.agents;
133
+ if (own(agents, 'default_subagent_model'))
134
+ result.model = 'pinned';
135
+ if (own(agents, 'default_subagent_reasoning_effort'))
136
+ result.effort = 'pinned';
137
+ for (const [key, value] of Object.entries(agents)) {
138
+ if (['default_subagent_model', 'default_subagent_reasoning_effort', 'enabled', 'max_threads',
139
+ 'max_concurrent_threads_per_session', 'interrupt_message'].includes(key))
140
+ continue;
141
+ if (!plain(value)) {
142
+ result = merge(result, unknown());
143
+ continue;
144
+ }
145
+ if (own(value, 'model'))
146
+ result.model = 'pinned';
147
+ if (present(value, ['model_reasoning_effort', 'reasoning_effort', 'effort']))
148
+ result.effort = 'pinned';
149
+ if (own(value, 'config_file'))
150
+ result = merge(result, unknown());
151
+ }
152
+ }
153
+ if (client === 'claude' && own(data, 'modelSettings')) {
154
+ if (!plain(data.modelSettings))
155
+ return merge(result, { model: 'unset', effort: 'unknown' });
156
+ for (const settings of Object.values(data.modelSettings)) {
157
+ if (!plain(settings))
158
+ result = merge(result, { model: 'unset', effort: 'unknown' });
159
+ else if (present(settings, ['effort', 'effortLevel', 'maxEffortLevel']))
160
+ result.effort = 'pinned';
161
+ }
162
+ }
163
+ return result;
164
+ }
165
+ function inspectOne(client, cwd, descriptor) {
166
+ if (!plain(descriptor) || typeof descriptor.path !== 'string' || !descriptor.path ||
167
+ descriptor.path.includes('\0') || Buffer.byteLength(descriptor.path) > 4096 ||
168
+ typeof cwd !== 'string' || !isAbsolute(cwd))
169
+ return unknown();
170
+ const path = resolve(cwd, descriptor.path);
171
+ if (secretPath(path))
172
+ return unknown();
173
+ const format = /\.([^.\\/]+)$/.exec(path)?.[1]?.toLowerCase();
174
+ if ((!format || !['json', 'toml', 'md'].includes(format)))
175
+ return unknown();
176
+ const file = explicitFile(path);
177
+ if (file.status === 'missing')
178
+ return unset();
179
+ if (file.status !== 'read')
180
+ return unknown();
181
+ try {
182
+ const data = format === 'json' ? JSON.parse(file.text) : format === 'toml' ? parseToml(file.text) : frontmatter(file.text);
183
+ return summarize(client, data);
184
+ }
185
+ catch {
186
+ return unknown();
187
+ }
188
+ }
189
+ /**
190
+ * Explain only explicitly selected files and a caller-supplied env snapshot.
191
+ * No secret values, paths, file bodies or parse errors leave this function.
192
+ * File absence is local evidence about that file, not source completeness.
193
+ */
194
+ export function explainSettings({ client, cwd = process.cwd(), files = [], environment = {}, clientVersion } = {}) {
195
+ const supported = client === 'codex' || client === 'claude';
196
+ const safeClient = supported ? client : 'unknown';
197
+ const requirements = supported ? provenanceRequirements[client] : { layers: [], environment: [] };
198
+ const seen = new Map();
199
+ if (supported && Array.isArray(files) && files.length <= MAX_FILES) {
200
+ for (const descriptor of files) {
201
+ if (!plain(descriptor) || (typeof descriptor.source !== 'string' || !allowedFiles[client].includes(descriptor.source)))
202
+ continue;
203
+ const value = inspectOne(client, cwd, descriptor);
204
+ seen.set(descriptor.source, seen.has(descriptor.source) ? merge(seen.get(descriptor.source), value) : value);
205
+ }
206
+ }
207
+ const env = {};
208
+ for (const name of requirements.environment)
209
+ env[name] = environment === null || typeof environment !== 'object' || Array.isArray(environment) ? 'unknown' : own(environment, name) ? 'set' : 'unset';
210
+ return {
211
+ client: safeClient,
212
+ client_version: typeof clientVersion === 'string' && /^\d{1,4}\.\d{1,4}\.\d{1,4}(?:-[A-Za-z0-9.-]{1,32})?$/.test(clientVersion) ? clientVersion : 'unknown',
213
+ layers: requirements.layers.map(source => ({ source, ...(seen.get(source) ?? unknown()) })),
214
+ environment: env,
215
+ host_extensions: 'unknown',
216
+ evidence_verified: false,
217
+ reason: 'effective_host_context_unobserved',
218
+ };
219
+ }
@@ -0,0 +1,32 @@
1
+ export interface ModelFact {
2
+ client: 'codex' | 'claude';
3
+ model: string;
4
+ description: string;
5
+ }
6
+ export interface ModelInfoSnapshot {
7
+ schema_version: 1;
8
+ fetched_at: string;
9
+ sources: Array<{
10
+ url: string;
11
+ sha256: string;
12
+ }>;
13
+ facts: ModelFact[];
14
+ }
15
+ export type ModelInfoStatus = 'bundled' | 'fresh' | 'stale' | 'unavailable' | 'invalid';
16
+ export interface LoadedModelInfo {
17
+ facts: ModelFact[];
18
+ status: ModelInfoStatus;
19
+ }
20
+ export declare const modelInfoUrls: readonly ["https://code.claude.com/docs/en/model-config.md", "https://learn.chatgpt.com/docs/models.md"];
21
+ /** Fixed endpoints only. Refresh is explicit; request-time recommendation never fetches pages. */
22
+ export declare function fetchModelInfo({ fetchImpl, now, timeoutMs }?: {
23
+ fetchImpl?: typeof fetch;
24
+ now?: number;
25
+ timeoutMs?: number;
26
+ }): Promise<ModelInfoSnapshot>;
27
+ /** Unknown cache data never becomes execution authority; unusable caches use bundled facts. */
28
+ export declare function loadModelInfo(path: unknown, { now }?: {
29
+ now?: number;
30
+ }): LoadedModelInfo;
31
+ /** Atomic replacement in an existing owner-private directory; unrelated files are never overwritten. */
32
+ export declare function saveModelInfo(path: string, snapshot: ModelInfoSnapshot): void;