@aisy/core 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 +202 -0
- package/dist/agent-loop/index.d.ts +4 -0
- package/dist/agent-loop/index.d.ts.map +1 -0
- package/dist/agent-loop/index.js +352 -0
- package/dist/agent-loop/index.js.map +1 -0
- package/dist/agent-loop/types.d.ts +183 -0
- package/dist/agent-loop/types.d.ts.map +1 -0
- package/dist/agent-loop/types.js +3 -0
- package/dist/agent-loop/types.js.map +1 -0
- package/dist/bin/aisy.d.ts +3 -0
- package/dist/bin/aisy.d.ts.map +1 -0
- package/dist/bin/aisy.js +14 -0
- package/dist/bin/aisy.js.map +1 -0
- package/dist/cli/index.d.ts +17 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +114 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-engine/index.d.ts +4 -0
- package/dist/context-engine/index.d.ts.map +1 -0
- package/dist/context-engine/index.js +126 -0
- package/dist/context-engine/index.js.map +1 -0
- package/dist/context-engine/types.d.ts +54 -0
- package/dist/context-engine/types.d.ts.map +1 -0
- package/dist/context-engine/types.js +4 -0
- package/dist/context-engine/types.js.map +1 -0
- package/dist/eval/index.d.ts +20 -0
- package/dist/eval/index.d.ts.map +1 -0
- package/dist/eval/index.js +128 -0
- package/dist/eval/index.js.map +1 -0
- package/dist/eval/types.d.ts +62 -0
- package/dist/eval/types.d.ts.map +1 -0
- package/dist/eval/types.js +17 -0
- package/dist/eval/types.js.map +1 -0
- package/dist/gateway/index.d.ts +5 -0
- package/dist/gateway/index.d.ts.map +1 -0
- package/dist/gateway/index.js +288 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/gateway/types.d.ts +194 -0
- package/dist/gateway/types.d.ts.map +1 -0
- package/dist/gateway/types.js +94 -0
- package/dist/gateway/types.js.map +1 -0
- package/dist/goals/index.d.ts +11 -0
- package/dist/goals/index.d.ts.map +1 -0
- package/dist/goals/index.js +21 -0
- package/dist/goals/index.js.map +1 -0
- package/dist/goals/types.d.ts +47 -0
- package/dist/goals/types.d.ts.map +1 -0
- package/dist/goals/types.js +5 -0
- package/dist/goals/types.js.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +50 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +5 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/index.js +215 -0
- package/dist/mcp/index.js.map +1 -0
- package/dist/mcp/types.d.ts +148 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/mcp/types.js +4 -0
- package/dist/mcp/types.js.map +1 -0
- package/dist/memory/index.d.ts +6 -0
- package/dist/memory/index.d.ts.map +1 -0
- package/dist/memory/index.js +419 -0
- package/dist/memory/index.js.map +1 -0
- package/dist/memory/types.d.ts +131 -0
- package/dist/memory/types.d.ts.map +1 -0
- package/dist/memory/types.js +33 -0
- package/dist/memory/types.js.map +1 -0
- package/dist/nightly/index.d.ts +4 -0
- package/dist/nightly/index.d.ts.map +1 -0
- package/dist/nightly/index.js +470 -0
- package/dist/nightly/index.js.map +1 -0
- package/dist/nightly/types.d.ts +326 -0
- package/dist/nightly/types.d.ts.map +1 -0
- package/dist/nightly/types.js +3 -0
- package/dist/nightly/types.js.map +1 -0
- package/dist/observability/index.d.ts +11 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +396 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/types.d.ts +139 -0
- package/dist/observability/types.d.ts.map +1 -0
- package/dist/observability/types.js +4 -0
- package/dist/observability/types.js.map +1 -0
- package/dist/onboarding/index.d.ts +16 -0
- package/dist/onboarding/index.d.ts.map +1 -0
- package/dist/onboarding/index.js +787 -0
- package/dist/onboarding/index.js.map +1 -0
- package/dist/onboarding/interactive.d.ts +23 -0
- package/dist/onboarding/interactive.d.ts.map +1 -0
- package/dist/onboarding/interactive.js +45 -0
- package/dist/onboarding/interactive.js.map +1 -0
- package/dist/onboarding/types.d.ts +388 -0
- package/dist/onboarding/types.d.ts.map +1 -0
- package/dist/onboarding/types.js +35 -0
- package/dist/onboarding/types.js.map +1 -0
- package/dist/orchestration/index.d.ts +8 -0
- package/dist/orchestration/index.d.ts.map +1 -0
- package/dist/orchestration/index.js +706 -0
- package/dist/orchestration/index.js.map +1 -0
- package/dist/orchestration/types.d.ts +391 -0
- package/dist/orchestration/types.d.ts.map +1 -0
- package/dist/orchestration/types.js +30 -0
- package/dist/orchestration/types.js.map +1 -0
- package/dist/personality/index.d.ts +65 -0
- package/dist/personality/index.d.ts.map +1 -0
- package/dist/personality/index.js +339 -0
- package/dist/personality/index.js.map +1 -0
- package/dist/personality/types.d.ts +103 -0
- package/dist/personality/types.d.ts.map +1 -0
- package/dist/personality/types.js +15 -0
- package/dist/personality/types.js.map +1 -0
- package/dist/provider/index.d.ts +4 -0
- package/dist/provider/index.d.ts.map +1 -0
- package/dist/provider/index.js +236 -0
- package/dist/provider/index.js.map +1 -0
- package/dist/provider/types.d.ts +180 -0
- package/dist/provider/types.d.ts.map +1 -0
- package/dist/provider/types.js +4 -0
- package/dist/provider/types.js.map +1 -0
- package/dist/runtime/agent-cards.d.ts +14 -0
- package/dist/runtime/agent-cards.d.ts.map +1 -0
- package/dist/runtime/agent-cards.js +90 -0
- package/dist/runtime/agent-cards.js.map +1 -0
- package/dist/runtime/agent-runner.d.ts +30 -0
- package/dist/runtime/agent-runner.d.ts.map +1 -0
- package/dist/runtime/agent-runner.js +37 -0
- package/dist/runtime/agent-runner.js.map +1 -0
- package/dist/runtime/budget.d.ts +15 -0
- package/dist/runtime/budget.d.ts.map +1 -0
- package/dist/runtime/budget.js +24 -0
- package/dist/runtime/budget.js.map +1 -0
- package/dist/runtime/delegation-driver.d.ts +11 -0
- package/dist/runtime/delegation-driver.d.ts.map +1 -0
- package/dist/runtime/delegation-driver.js +132 -0
- package/dist/runtime/delegation-driver.js.map +1 -0
- package/dist/runtime/exact-cache.d.ts +10 -0
- package/dist/runtime/exact-cache.d.ts.map +1 -0
- package/dist/runtime/exact-cache.js +30 -0
- package/dist/runtime/exact-cache.js.map +1 -0
- package/dist/runtime/execute-tool.d.ts +29 -0
- package/dist/runtime/execute-tool.d.ts.map +1 -0
- package/dist/runtime/execute-tool.js +80 -0
- package/dist/runtime/execute-tool.js.map +1 -0
- package/dist/runtime/guardian.d.ts +9 -0
- package/dist/runtime/guardian.d.ts.map +1 -0
- package/dist/runtime/guardian.js +41 -0
- package/dist/runtime/guardian.js.map +1 -0
- package/dist/runtime/hook-gate.d.ts +17 -0
- package/dist/runtime/hook-gate.d.ts.map +1 -0
- package/dist/runtime/hook-gate.js +56 -0
- package/dist/runtime/hook-gate.js.map +1 -0
- package/dist/runtime/memory-adapter.d.ts +6 -0
- package/dist/runtime/memory-adapter.d.ts.map +1 -0
- package/dist/runtime/memory-adapter.js +38 -0
- package/dist/runtime/memory-adapter.js.map +1 -0
- package/dist/runtime/nightly-adapters.d.ts +48 -0
- package/dist/runtime/nightly-adapters.d.ts.map +1 -0
- package/dist/runtime/nightly-adapters.js +139 -0
- package/dist/runtime/nightly-adapters.js.map +1 -0
- package/dist/runtime/nightly-generator.d.ts +10 -0
- package/dist/runtime/nightly-generator.d.ts.map +1 -0
- package/dist/runtime/nightly-generator.js +335 -0
- package/dist/runtime/nightly-generator.js.map +1 -0
- package/dist/runtime/onboarding-node.d.ts +6 -0
- package/dist/runtime/onboarding-node.d.ts.map +1 -0
- package/dist/runtime/onboarding-node.js +356 -0
- package/dist/runtime/onboarding-node.js.map +1 -0
- package/dist/runtime/provider-anthropic.d.ts +43 -0
- package/dist/runtime/provider-anthropic.d.ts.map +1 -0
- package/dist/runtime/provider-anthropic.js +148 -0
- package/dist/runtime/provider-anthropic.js.map +1 -0
- package/dist/runtime/provider-cli.d.ts +18 -0
- package/dist/runtime/provider-cli.d.ts.map +1 -0
- package/dist/runtime/provider-cli.js +73 -0
- package/dist/runtime/provider-cli.js.map +1 -0
- package/dist/runtime/provider-openai.d.ts +30 -0
- package/dist/runtime/provider-openai.d.ts.map +1 -0
- package/dist/runtime/provider-openai.js +114 -0
- package/dist/runtime/provider-openai.js.map +1 -0
- package/dist/runtime/providers.d.ts +43 -0
- package/dist/runtime/providers.d.ts.map +1 -0
- package/dist/runtime/providers.js +72 -0
- package/dist/runtime/providers.js.map +1 -0
- package/dist/runtime/sandbox-bash.d.ts +21 -0
- package/dist/runtime/sandbox-bash.d.ts.map +1 -0
- package/dist/runtime/sandbox-bash.js +51 -0
- package/dist/runtime/sandbox-bash.js.map +1 -0
- package/dist/runtime/scoped-tool-executor.d.ts +10 -0
- package/dist/runtime/scoped-tool-executor.d.ts.map +1 -0
- package/dist/runtime/scoped-tool-executor.js +30 -0
- package/dist/runtime/scoped-tool-executor.js.map +1 -0
- package/dist/runtime/session-log.d.ts +6 -0
- package/dist/runtime/session-log.d.ts.map +1 -0
- package/dist/runtime/session-log.js +54 -0
- package/dist/runtime/session-log.js.map +1 -0
- package/dist/runtime/settings.d.ts +24 -0
- package/dist/runtime/settings.d.ts.map +1 -0
- package/dist/runtime/settings.js +29 -0
- package/dist/runtime/settings.js.map +1 -0
- package/dist/runtime/spawn-plan.d.ts +13 -0
- package/dist/runtime/spawn-plan.d.ts.map +1 -0
- package/dist/runtime/spawn-plan.js +107 -0
- package/dist/runtime/spawn-plan.js.map +1 -0
- package/dist/runtime/spend.d.ts +41 -0
- package/dist/runtime/spend.d.ts.map +1 -0
- package/dist/runtime/spend.js +0 -0
- package/dist/runtime/spend.js.map +1 -0
- package/dist/runtime/sub-agent-runner.d.ts +19 -0
- package/dist/runtime/sub-agent-runner.d.ts.map +1 -0
- package/dist/runtime/sub-agent-runner.js +47 -0
- package/dist/runtime/sub-agent-runner.js.map +1 -0
- package/dist/safety/grants.d.ts +7 -0
- package/dist/safety/grants.d.ts.map +1 -0
- package/dist/safety/grants.js +53 -0
- package/dist/safety/grants.js.map +1 -0
- package/dist/safety/index.d.ts +72 -0
- package/dist/safety/index.d.ts.map +1 -0
- package/dist/safety/index.js +464 -0
- package/dist/safety/index.js.map +1 -0
- package/dist/safety/types.d.ts +254 -0
- package/dist/safety/types.d.ts.map +1 -0
- package/dist/safety/types.js +3 -0
- package/dist/safety/types.js.map +1 -0
- package/dist/skills/index.d.ts +4 -0
- package/dist/skills/index.d.ts.map +1 -0
- package/dist/skills/index.js +463 -0
- package/dist/skills/index.js.map +1 -0
- package/dist/skills/types.d.ts +177 -0
- package/dist/skills/types.d.ts.map +1 -0
- package/dist/skills/types.js +3 -0
- package/dist/skills/types.js.map +1 -0
- package/dist/testing/clock.d.ts +8 -0
- package/dist/testing/clock.d.ts.map +1 -0
- package/dist/testing/clock.js +13 -0
- package/dist/testing/clock.js.map +1 -0
- package/dist/testing/effect-verifier.d.ts +15 -0
- package/dist/testing/effect-verifier.d.ts.map +1 -0
- package/dist/testing/effect-verifier.js +27 -0
- package/dist/testing/effect-verifier.js.map +1 -0
- package/dist/testing/index.d.ts +5 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +5 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/testing/provider-fake.d.ts +14 -0
- package/dist/testing/provider-fake.d.ts.map +1 -0
- package/dist/testing/provider-fake.js +18 -0
- package/dist/testing/provider-fake.js.map +1 -0
- package/dist/testing/sandbox-stub.d.ts +15 -0
- package/dist/testing/sandbox-stub.d.ts.map +1 -0
- package/dist/testing/sandbox-stub.js +15 -0
- package/dist/testing/sandbox-stub.js.map +1 -0
- package/dist/tools/index.d.ts +11 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +0 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/types.d.ts +138 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +4 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/triggers/index.d.ts +4 -0
- package/dist/triggers/index.d.ts.map +1 -0
- package/dist/triggers/index.js +187 -0
- package/dist/triggers/index.js.map +1 -0
- package/dist/triggers/types.d.ts +74 -0
- package/dist/triggers/types.d.ts.map +1 -0
- package/dist/triggers/types.js +5 -0
- package/dist/triggers/types.js.map +1 -0
- package/package.json +36 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { REQUIRED_ENV_KEYS, SCAFFOLD_FILES, MEMORY_TREE_FILES, MEMORY_TREE_DIRS } from './types.js';
|
|
3
|
+
import { runTelegramPairing } from './interactive.js';
|
|
4
|
+
export { REQUIRED_ENV_KEYS, SCAFFOLD_FILES, MEMORY_TREE_FILES, MEMORY_TREE_DIRS, } from './types.js';
|
|
5
|
+
const TIERS = ['reasoning', 'critique', 'routine'];
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
// Redaction helper. The component handles credentials and exports bundles, so
|
|
8
|
+
// every sink runs through here: a secret VALUE never appears in detail, an
|
|
9
|
+
// InitOutcome, a journal tail, or a diagnostics file (CSO-M3, AC-13-4/5/15/16).
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
function redactWith(values, text) {
|
|
12
|
+
let out = text;
|
|
13
|
+
for (const v of values) {
|
|
14
|
+
if (v.length > 0)
|
|
15
|
+
out = out.split(v).join('«redacted»');
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
// ===========================================================================
|
|
20
|
+
// makeOnboardingOps — init / doctor / diagnostics (spec §3, §5.1–5.2, §5.5)
|
|
21
|
+
// All deterministic; the model is never on this path.
|
|
22
|
+
// ===========================================================================
|
|
23
|
+
export function makeOnboardingOps(deps) {
|
|
24
|
+
const emit = (event, payload) => deps.events?.emit(event, payload);
|
|
25
|
+
const secretValues = () => deps.vault.secretValues();
|
|
26
|
+
const redact = (s) => redactWith(secretValues(), s);
|
|
27
|
+
// ---- prereq + .env helpers --------------------------------------------
|
|
28
|
+
function parseEnvBody(body) {
|
|
29
|
+
const keys = new Set();
|
|
30
|
+
for (const line of body.split(/\r?\n/)) {
|
|
31
|
+
const m = /^([A-Z0-9_]+)=(.*)$/.exec(line.trim());
|
|
32
|
+
if (m && m[1] !== undefined && (m[2] ?? '').length > 0)
|
|
33
|
+
keys.add(m[1]);
|
|
34
|
+
}
|
|
35
|
+
return keys;
|
|
36
|
+
}
|
|
37
|
+
function envValueOf(key) {
|
|
38
|
+
return deps.env?.[key] ?? '';
|
|
39
|
+
}
|
|
40
|
+
// -------------------------------------------------------------------------
|
|
41
|
+
// init (§5.1) — resumable: every step records an InitOutcome; satisfied
|
|
42
|
+
// steps yield already-present/skipped and write nothing without --force.
|
|
43
|
+
// -------------------------------------------------------------------------
|
|
44
|
+
async function init(opts) {
|
|
45
|
+
emit('init.started');
|
|
46
|
+
const outcomes = [];
|
|
47
|
+
const scaffolded = [];
|
|
48
|
+
const force = opts.force === true;
|
|
49
|
+
let failed = false;
|
|
50
|
+
// Interactive collect (ADR-0049): when a prompt is wired and the operator
|
|
51
|
+
// did not opt into non-interactive/--yes, prompt for missing required
|
|
52
|
+
// secrets and pair the Telegram chat. Collected values override env for the
|
|
53
|
+
// rest of init (validation, vault seed). Already-set keys are not re-asked.
|
|
54
|
+
const collected = {};
|
|
55
|
+
const valueOf = (key) => collected[key] ?? envValueOf(key);
|
|
56
|
+
// ADR-0050: when a provider catalog is injected, the interactive flow offers
|
|
57
|
+
// a provider/model picker (single model or per-tier) and persists
|
|
58
|
+
// providers.json, instead of the legacy per-tier key prompts.
|
|
59
|
+
// catalogSelections drives provider-aware validation in step [2].
|
|
60
|
+
let catalogSelections = [];
|
|
61
|
+
const interactive = deps.prompt !== undefined && opts.nonInteractive !== true && opts.yes !== true;
|
|
62
|
+
if (interactive && deps.prompt) {
|
|
63
|
+
const p = deps.prompt;
|
|
64
|
+
p.info('Настройка Aisy — отвечай на вопросы (Enter — пропустить шаг).');
|
|
65
|
+
const catalog = deps.providerCatalog;
|
|
66
|
+
if (catalog && catalog.length > 0 && deps.providersOut) {
|
|
67
|
+
p.info('Доступные провайдеры:');
|
|
68
|
+
catalog.forEach((e, i) => p.info(` ${i + 1}. ${e.label}`));
|
|
69
|
+
const pickOne = async (prefix) => {
|
|
70
|
+
const raw = (await p.ask(`${prefix}провайдер (номер)`, { default: '1' })).trim();
|
|
71
|
+
const n = Number.parseInt(raw, 10);
|
|
72
|
+
const idx = Number.isFinite(n) && n >= 1 && n <= catalog.length ? n - 1 : 0;
|
|
73
|
+
const entry = catalog[idx];
|
|
74
|
+
const defModel = entry.defaultModels?.[0];
|
|
75
|
+
const model = (await p.ask(`Модель (${entry.label})`, defModel ? { default: defModel } : {})).trim() || (defModel ?? '');
|
|
76
|
+
if (entry.needsKey && entry.keyEnv) {
|
|
77
|
+
const key = (await p.secret(`API-ключ (${entry.label}):`)).trim();
|
|
78
|
+
if (key.length > 0)
|
|
79
|
+
collected[entry.keyEnv] = key;
|
|
80
|
+
// Known providers carry a catalog default base URL (buildProvider
|
|
81
|
+
// falls back to it); only a custom endpoint must be asked.
|
|
82
|
+
if (entry.defaultBaseUrl === undefined) {
|
|
83
|
+
const bu = (await p.ask(`Base URL (${entry.label})`)).trim();
|
|
84
|
+
if (bu.length > 0)
|
|
85
|
+
collected[`AISY_PROVIDER_${entry.id.toUpperCase()}_BASE_URL`] = bu;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { provider: entry.id, model };
|
|
89
|
+
};
|
|
90
|
+
const single = await p.confirm('Одна модель для всех тиров?', { default: true });
|
|
91
|
+
let config;
|
|
92
|
+
if (single) {
|
|
93
|
+
const sel = await pickOne('');
|
|
94
|
+
config = { default: sel };
|
|
95
|
+
catalogSelections = [sel];
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
const reasoning = await pickOne('reasoning · ');
|
|
99
|
+
const critique = await pickOne('critique · ');
|
|
100
|
+
const routine = await pickOne('routine · ');
|
|
101
|
+
config = { tiers: { reasoning, critique, routine } };
|
|
102
|
+
catalogSelections = [reasoning, critique, routine];
|
|
103
|
+
}
|
|
104
|
+
deps.providersOut.write(config);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
// Legacy: prompt the per-tier provider keys (no catalog injected).
|
|
108
|
+
for (const tier of TIERS) {
|
|
109
|
+
const key = `AISY_PROVIDER_${tier.toUpperCase()}_KEY`;
|
|
110
|
+
if (valueOf(key).length === 0) {
|
|
111
|
+
const v = (await p.secret(`API-ключ провайдера (${tier}):`)).trim();
|
|
112
|
+
if (v.length > 0)
|
|
113
|
+
collected[key] = v;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (valueOf('AISY_TELEGRAM_BOT_TOKEN').length === 0) {
|
|
118
|
+
const t = (await p.secret('Telegram bot token:')).trim();
|
|
119
|
+
if (t.length > 0)
|
|
120
|
+
collected['AISY_TELEGRAM_BOT_TOKEN'] = t;
|
|
121
|
+
}
|
|
122
|
+
if (valueOf('AISY_TELEGRAM_CHAT_ID').length === 0) {
|
|
123
|
+
const token = valueOf('AISY_TELEGRAM_BOT_TOKEN');
|
|
124
|
+
if (token.length > 0) {
|
|
125
|
+
const gu = deps.validators.telegramGetUpdates;
|
|
126
|
+
const chatId = await runTelegramPairing(token, {
|
|
127
|
+
prompt: p,
|
|
128
|
+
...(gu ? { getUpdates: (t) => gu(t) } : {}),
|
|
129
|
+
clock: () => Date.now(),
|
|
130
|
+
genCode: () => `AISY-${randomUUID().slice(0, 4).toUpperCase()}`,
|
|
131
|
+
sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
|
|
132
|
+
});
|
|
133
|
+
if (chatId)
|
|
134
|
+
collected['AISY_TELEGRAM_CHAT_ID'] = chatId;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
for (const key of ['AISY_MEMORY_ROOT', 'AISY_DB_PATH']) {
|
|
138
|
+
if (valueOf(key).length === 0) {
|
|
139
|
+
const v = (await p.ask(`Путь ${key}`)).trim();
|
|
140
|
+
if (v.length > 0)
|
|
141
|
+
collected[key] = v;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Seed the redactor BEFORE any redact() on a failure detail. The vault is
|
|
146
|
+
// only persisted later (step [4], gated on success), so until then redact()
|
|
147
|
+
// over deps.vault.secretValues() would be a guaranteed no-op. Here we mask
|
|
148
|
+
// against the env secret VALUES directly so a rejection detail can never
|
|
149
|
+
// echo a secret, regardless of validation outcome (CSO-M3, AC-13-4/5).
|
|
150
|
+
const envSecretValues = new Set();
|
|
151
|
+
for (const key of REQUIRED_ENV_KEYS) {
|
|
152
|
+
const value = valueOf(key);
|
|
153
|
+
if (value.length > 0)
|
|
154
|
+
envSecretValues.add(value);
|
|
155
|
+
}
|
|
156
|
+
// Catalog-collected provider keys live outside REQUIRED_ENV_KEYS — mask them
|
|
157
|
+
// too so a rejection detail can never echo a provider secret (CSO-M3).
|
|
158
|
+
for (const value of Object.values(collected)) {
|
|
159
|
+
if (value.length > 0)
|
|
160
|
+
envSecretValues.add(value);
|
|
161
|
+
}
|
|
162
|
+
const redactInit = (s) => redactWith(envSecretValues, s);
|
|
163
|
+
// [1] Detect prerequisites — fail-closed with an actionable message (§7).
|
|
164
|
+
for (const tool of ['node', 'pnpm', 'docker']) {
|
|
165
|
+
const v = deps.prereqs.version(tool);
|
|
166
|
+
if (v === null) {
|
|
167
|
+
outcomes.push({ step: `prereq.${tool}`, result: 'failed', detail: `${tool} not found — install and re-run aisy init` });
|
|
168
|
+
failed = true;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
outcomes.push({ step: `prereq.${tool}`, result: 'done' });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
// [2] Validate credentials via INJECTED validators (no real network).
|
|
175
|
+
// A secret VALUE is never written into detail — only status (AC-13-4/5).
|
|
176
|
+
if (catalogSelections.length > 0) {
|
|
177
|
+
// Catalog picker (ADR-0050): validate each DISTINCT chosen provider via the
|
|
178
|
+
// provider-aware ping. CLI providers carry no key and are skipped.
|
|
179
|
+
const catalog = deps.providerCatalog ?? [];
|
|
180
|
+
const seen = new Set();
|
|
181
|
+
for (const sel of catalogSelections) {
|
|
182
|
+
if (seen.has(sel.provider))
|
|
183
|
+
continue;
|
|
184
|
+
seen.add(sel.provider);
|
|
185
|
+
const entry = catalog.find((e) => e.id === sel.provider);
|
|
186
|
+
if (!entry || !entry.needsKey || !entry.keyEnv) {
|
|
187
|
+
outcomes.push({ step: `validate.provider.${sel.provider}`, result: 'done' });
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
const key = valueOf(entry.keyEnv);
|
|
191
|
+
const baseUrl = valueOf(`AISY_PROVIDER_${entry.id.toUpperCase()}_BASE_URL`) || entry.defaultBaseUrl;
|
|
192
|
+
const ping = deps.validators.pingCatalogProvider
|
|
193
|
+
? await deps.validators.pingCatalogProvider({ providerId: sel.provider, key, ...(baseUrl ? { baseUrl } : {}) })
|
|
194
|
+
: { ok: key.length > 0 };
|
|
195
|
+
if (ping.ok) {
|
|
196
|
+
outcomes.push({ step: `validate.provider.${sel.provider}`, result: 'done' });
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
outcomes.push({
|
|
200
|
+
step: `validate.provider.${sel.provider}`,
|
|
201
|
+
result: 'failed',
|
|
202
|
+
detail: redactInit(`${sel.provider} key rejected (HTTP ${ping.httpStatus ?? '???'})`),
|
|
203
|
+
});
|
|
204
|
+
failed = true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
for (const tier of TIERS) {
|
|
210
|
+
const key = valueOf(`AISY_PROVIDER_${tier.toUpperCase()}_KEY`);
|
|
211
|
+
const ping = await deps.validators.pingProvider(tier, key);
|
|
212
|
+
if (ping.ok) {
|
|
213
|
+
outcomes.push({ step: `validate.provider.${tier}`, result: 'done' });
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
outcomes.push({
|
|
217
|
+
step: `validate.provider.${tier}`,
|
|
218
|
+
result: 'failed',
|
|
219
|
+
detail: redactInit(`${tier}-tier key rejected (HTTP ${ping.httpStatus ?? '???'})`),
|
|
220
|
+
});
|
|
221
|
+
failed = true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
{
|
|
226
|
+
const token = valueOf('AISY_TELEGRAM_BOT_TOKEN');
|
|
227
|
+
const me = await deps.validators.telegramGetMe(token);
|
|
228
|
+
if (me.ok) {
|
|
229
|
+
outcomes.push({ step: 'validate.telegram-token', result: 'done' });
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
outcomes.push({
|
|
233
|
+
step: 'validate.telegram-token',
|
|
234
|
+
result: 'failed',
|
|
235
|
+
detail: redactInit(`Telegram token rejected (HTTP ${me.httpStatus ?? '???'})`),
|
|
236
|
+
});
|
|
237
|
+
failed = true;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// [3] Scaffold files — only if absent (or populated→skip unless --force).
|
|
241
|
+
const scaffoldFile = (path) => {
|
|
242
|
+
const present = deps.fs.exists(path);
|
|
243
|
+
let populated = present && deps.fs.isPopulated(path);
|
|
244
|
+
// A template-only .env (every required key has an empty value) is NOT a
|
|
245
|
+
// real config: it must not be reported already-present, or doctor's
|
|
246
|
+
// env.required-keys check is silently masked. Re-scaffold so the operator
|
|
247
|
+
// gets a fresh template and the missing values are surfaced (AC-13-9).
|
|
248
|
+
if (present && populated && path === '.env' && parseEnvBody(deps.fs.read(path)).size === 0) {
|
|
249
|
+
populated = false;
|
|
250
|
+
}
|
|
251
|
+
if (present && populated && !force) {
|
|
252
|
+
outcomes.push({ step: `scaffold.${path}`, result: 'already-present' });
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
if (present && !populated && path !== '.env' && !force) {
|
|
256
|
+
// present-but-empty (e.g. a crash mid-write) — leave it, treat as present.
|
|
257
|
+
outcomes.push({ step: `scaffold.${path}`, result: 'already-present' });
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
deps.fs.write(path, templateFor(path));
|
|
261
|
+
scaffolded.push(path);
|
|
262
|
+
outcomes.push({ step: `scaffold.${path}`, result: 'done' });
|
|
263
|
+
emit('init.step', { step: `scaffold.${path}` });
|
|
264
|
+
};
|
|
265
|
+
for (const f of SCAFFOLD_FILES)
|
|
266
|
+
scaffoldFile(f);
|
|
267
|
+
for (const f of MEMORY_TREE_FILES)
|
|
268
|
+
scaffoldFile(f);
|
|
269
|
+
for (const d of MEMORY_TREE_DIRS) {
|
|
270
|
+
if (deps.fs.exists(d)) {
|
|
271
|
+
outcomes.push({ step: `scaffold.${d}`, result: 'already-present' });
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
deps.fs.mkdirp(d);
|
|
275
|
+
outcomes.push({ step: `scaffold.${d}`, result: 'done' });
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// [4] Seed vault (Safety 05) with validated secrets — never logged.
|
|
279
|
+
if (!failed) {
|
|
280
|
+
for (const key of REQUIRED_ENV_KEYS) {
|
|
281
|
+
const value = valueOf(key);
|
|
282
|
+
if (value.length > 0)
|
|
283
|
+
deps.vault.seed(key, value);
|
|
284
|
+
}
|
|
285
|
+
// Catalog picker (ADR-0050) collects provider keys / base URLs under
|
|
286
|
+
// names outside REQUIRED_ENV_KEYS (e.g. AISY_PROVIDER_DEEPSEEK_KEY); seed
|
|
287
|
+
// those too so `aisy run` can resolve them from the vault.
|
|
288
|
+
const required = REQUIRED_ENV_KEYS;
|
|
289
|
+
for (const [key, value] of Object.entries(collected)) {
|
|
290
|
+
if (value.length > 0 && !required.includes(key))
|
|
291
|
+
deps.vault.seed(key, value);
|
|
292
|
+
}
|
|
293
|
+
outcomes.push({ step: 'vault.seed', result: 'done' });
|
|
294
|
+
}
|
|
295
|
+
else {
|
|
296
|
+
outcomes.push({ step: 'vault.seed', result: 'skipped' });
|
|
297
|
+
}
|
|
298
|
+
// [5] Initialize stores: Memory.rebuildFromFiles() → SQLite FTS5 index.
|
|
299
|
+
if (!failed) {
|
|
300
|
+
await deps.memory.rebuildFromFiles();
|
|
301
|
+
outcomes.push({ step: 'stores.memory-index', result: 'done' });
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
outcomes.push({ step: 'stores.memory-index', result: 'skipped' });
|
|
305
|
+
}
|
|
306
|
+
const completed = !failed;
|
|
307
|
+
emit('init.completed', { completed });
|
|
308
|
+
return { completed, outcomes, scaffolded };
|
|
309
|
+
}
|
|
310
|
+
// -------------------------------------------------------------------------
|
|
311
|
+
// doctor (§5.2) — read-only by default; --fix applies only fixable &&
|
|
312
|
+
// non-destructive repairs. ok:false iff any high/critical check fails.
|
|
313
|
+
// -------------------------------------------------------------------------
|
|
314
|
+
async function runChecks(opts) {
|
|
315
|
+
const checks = [];
|
|
316
|
+
const add = (c) => {
|
|
317
|
+
checks.push(c);
|
|
318
|
+
emit('doctor.check', { id: c.id, status: c.status });
|
|
319
|
+
};
|
|
320
|
+
const fix = opts.fix === true;
|
|
321
|
+
// ADR-0050: when a providers.json exists, doctor validates the CHOSEN
|
|
322
|
+
// providers (keys from the merged env/vault map) instead of legacy tiers.
|
|
323
|
+
const provCfg = deps.providersIn?.read() ?? null;
|
|
324
|
+
const catalog = deps.providerCatalog ?? [];
|
|
325
|
+
const chosenSelections = provCfg
|
|
326
|
+
? [
|
|
327
|
+
...(provCfg.default ? [provCfg.default] : []),
|
|
328
|
+
...(provCfg.tiers ? Object.values(provCfg.tiers) : []),
|
|
329
|
+
]
|
|
330
|
+
: [];
|
|
331
|
+
const distinctChosen = [...new Map(chosenSelections.map((s) => [s.provider, s])).values()];
|
|
332
|
+
const requiredKeys = provCfg
|
|
333
|
+
? [
|
|
334
|
+
'AISY_TELEGRAM_BOT_TOKEN',
|
|
335
|
+
'AISY_TELEGRAM_CHAT_ID',
|
|
336
|
+
'AISY_MEMORY_ROOT',
|
|
337
|
+
'AISY_DB_PATH',
|
|
338
|
+
...distinctChosen
|
|
339
|
+
.map((s) => catalog.find((e) => e.id === s.provider))
|
|
340
|
+
.filter((e) => !!e && e.needsKey && !!e.keyEnv)
|
|
341
|
+
.map((e) => e.keyEnv),
|
|
342
|
+
]
|
|
343
|
+
: REQUIRED_ENV_KEYS;
|
|
344
|
+
// env (critical) — required keys present.
|
|
345
|
+
{
|
|
346
|
+
let missing;
|
|
347
|
+
if (provCfg) {
|
|
348
|
+
missing = requiredKeys.filter((k) => envValueOf(k).length === 0);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
const body = deps.fs.exists('.env') ? deps.fs.read('.env') : '';
|
|
352
|
+
const present = parseEnvBody(body);
|
|
353
|
+
missing = requiredKeys.filter((k) => !present.has(k));
|
|
354
|
+
}
|
|
355
|
+
const ok = missing.length === 0;
|
|
356
|
+
add({
|
|
357
|
+
id: 'env.required-keys',
|
|
358
|
+
domain: 'env',
|
|
359
|
+
status: ok ? 'pass' : 'fail',
|
|
360
|
+
severity: 'critical',
|
|
361
|
+
detail: ok ? 'all required keys present' : `missing required keys: ${missing.join(', ')}`,
|
|
362
|
+
fixable: false,
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
// providers (high) — reachability ping. Catalog install ⇒ ping the chosen
|
|
366
|
+
// providers; otherwise the legacy per-tier ping.
|
|
367
|
+
if (provCfg) {
|
|
368
|
+
for (const sel of distinctChosen) {
|
|
369
|
+
const entry = catalog.find((e) => e.id === sel.provider);
|
|
370
|
+
if (!entry || !entry.needsKey || !entry.keyEnv) {
|
|
371
|
+
add({
|
|
372
|
+
id: `providers.${sel.provider}.reachable`,
|
|
373
|
+
domain: 'providers',
|
|
374
|
+
status: 'pass',
|
|
375
|
+
severity: 'high',
|
|
376
|
+
detail: `${sel.provider} needs no key`,
|
|
377
|
+
fixable: false,
|
|
378
|
+
});
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
const key = envValueOf(entry.keyEnv);
|
|
382
|
+
const baseUrl = envValueOf(`AISY_PROVIDER_${entry.id.toUpperCase()}_BASE_URL`) || entry.defaultBaseUrl;
|
|
383
|
+
const ping = deps.validators.pingCatalogProvider
|
|
384
|
+
? await deps.validators.pingCatalogProvider({ providerId: sel.provider, key, ...(baseUrl ? { baseUrl } : {}) })
|
|
385
|
+
: { ok: key.length > 0 };
|
|
386
|
+
add({
|
|
387
|
+
id: `providers.${sel.provider}.reachable`,
|
|
388
|
+
domain: 'providers',
|
|
389
|
+
status: ping.ok ? 'pass' : 'fail',
|
|
390
|
+
severity: 'high',
|
|
391
|
+
detail: ping.ok ? `${sel.provider} key reachable` : redact(`${sel.provider} key rejected (HTTP ${ping.httpStatus ?? '???'})`),
|
|
392
|
+
fixable: false,
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
for (const tier of TIERS) {
|
|
398
|
+
const key = envValueOf(`AISY_PROVIDER_${tier.toUpperCase()}_KEY`);
|
|
399
|
+
const ping = await deps.validators.pingProvider(tier, key);
|
|
400
|
+
add({
|
|
401
|
+
id: `providers.${tier}.reachable`,
|
|
402
|
+
domain: 'providers',
|
|
403
|
+
status: ping.ok ? 'pass' : 'fail',
|
|
404
|
+
severity: 'high',
|
|
405
|
+
detail: ping.ok ? `${tier} key reachable` : redact(`${tier} key rejected (HTTP ${ping.httpStatus ?? '???'})`),
|
|
406
|
+
fixable: false,
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
// telegram (critical) — getMe + exactly one allowlisted chat_id (spec §4 matrix).
|
|
411
|
+
{
|
|
412
|
+
const token = envValueOf('AISY_TELEGRAM_BOT_TOKEN');
|
|
413
|
+
const me = await deps.validators.telegramGetMe(token);
|
|
414
|
+
add({
|
|
415
|
+
id: 'telegram.token-valid',
|
|
416
|
+
domain: 'telegram',
|
|
417
|
+
status: me.ok ? 'pass' : 'fail',
|
|
418
|
+
severity: 'critical',
|
|
419
|
+
detail: me.ok ? 'bot token valid' : redact(`token rejected (HTTP ${me.httpStatus ?? '???'})`),
|
|
420
|
+
fixable: false,
|
|
421
|
+
});
|
|
422
|
+
// Exactly one allowlisted chat_id must be configured: zero leaves the bot
|
|
423
|
+
// open to any chat; more than one is not the single-operator default.
|
|
424
|
+
const chatIds = envValueOf('AISY_TELEGRAM_CHAT_ID')
|
|
425
|
+
.split(/[\s,]+/)
|
|
426
|
+
.map((c) => c.trim())
|
|
427
|
+
.filter((c) => c.length > 0);
|
|
428
|
+
const oneChatId = chatIds.length === 1;
|
|
429
|
+
add({
|
|
430
|
+
id: 'telegram.chat-id-allowlist',
|
|
431
|
+
domain: 'telegram',
|
|
432
|
+
status: oneChatId ? 'pass' : 'fail',
|
|
433
|
+
severity: 'critical',
|
|
434
|
+
detail: oneChatId
|
|
435
|
+
? 'exactly one allowlisted chat_id set'
|
|
436
|
+
: chatIds.length === 0
|
|
437
|
+
? 'no allowlisted chat_id set (AISY_TELEGRAM_CHAT_ID empty)'
|
|
438
|
+
: `expected exactly one allowlisted chat_id, found ${chatIds.length}`,
|
|
439
|
+
fixable: false,
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
// memory (high) — tree exists + integrity_check. Fixable via rebuild.
|
|
443
|
+
{
|
|
444
|
+
const integrity = await deps.memory.integrityCheck();
|
|
445
|
+
const fixId = 'memory.rebuild-index';
|
|
446
|
+
if (!integrity.ok && fix) {
|
|
447
|
+
// FIXABLE, NON-DESTRUCTIVE: rebuild re-applies the forget invariant;
|
|
448
|
+
// it never deletes a fact (AC-13-10/11).
|
|
449
|
+
await deps.memory.rebuildFromFiles();
|
|
450
|
+
emit('doctor.fix', { fixId });
|
|
451
|
+
const after = await deps.memory.integrityCheck();
|
|
452
|
+
add({
|
|
453
|
+
id: 'memory.integrity',
|
|
454
|
+
domain: 'memory',
|
|
455
|
+
status: after.ok ? 'pass' : 'fail',
|
|
456
|
+
severity: 'high',
|
|
457
|
+
detail: after.ok ? 'index rebuilt; integrity restored' : redact(after.detail ?? 'integrity check failed'),
|
|
458
|
+
fixable: true,
|
|
459
|
+
fixId,
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
else {
|
|
463
|
+
add({
|
|
464
|
+
id: 'memory.integrity',
|
|
465
|
+
domain: 'memory',
|
|
466
|
+
status: integrity.ok ? 'pass' : 'fail',
|
|
467
|
+
severity: 'high',
|
|
468
|
+
detail: integrity.ok ? 'memory index consistent' : redact(integrity.detail ?? 'integrity check failed'),
|
|
469
|
+
fixable: true,
|
|
470
|
+
fixId,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
// vault (critical) — loads + seeded secrets decrypt.
|
|
475
|
+
{
|
|
476
|
+
const ok = deps.vault.loads();
|
|
477
|
+
add({
|
|
478
|
+
id: 'vault.loads',
|
|
479
|
+
domain: 'vault',
|
|
480
|
+
status: ok ? 'pass' : 'fail',
|
|
481
|
+
severity: 'critical',
|
|
482
|
+
detail: ok ? 'vault loads; seeded secrets decrypt' : 'vault failed to load',
|
|
483
|
+
fixable: false,
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
// sandbox (high) — folds pnpm sandbox:doctor (AC-13-14).
|
|
487
|
+
{
|
|
488
|
+
const up = deps.sandbox.daemonUp();
|
|
489
|
+
const img = deps.sandbox.imagePresent();
|
|
490
|
+
const caps = deps.sandbox.capsDropped();
|
|
491
|
+
const ok = up && img && caps;
|
|
492
|
+
const detail = !up
|
|
493
|
+
? 'Docker daemon down'
|
|
494
|
+
: !img
|
|
495
|
+
? 'sandbox image absent'
|
|
496
|
+
: !caps
|
|
497
|
+
? 'caps not dropped'
|
|
498
|
+
: `runtime=${deps.sandbox.runtime() ?? 'standard'}; caps dropped`;
|
|
499
|
+
add({
|
|
500
|
+
id: 'sandbox.docker',
|
|
501
|
+
domain: 'sandbox',
|
|
502
|
+
status: ok ? 'pass' : 'fail',
|
|
503
|
+
severity: 'high',
|
|
504
|
+
detail,
|
|
505
|
+
fixable: false,
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
// mcp (high) — allowlist parses + descriptor-hash pins match (AC-13-13).
|
|
509
|
+
{
|
|
510
|
+
const parses = deps.mcp.allowlistParses();
|
|
511
|
+
const hashes = deps.mcp.descriptorHashesMatch();
|
|
512
|
+
const ok = parses && hashes;
|
|
513
|
+
add({
|
|
514
|
+
id: 'mcp.descriptor-pins',
|
|
515
|
+
domain: 'mcp',
|
|
516
|
+
status: ok ? 'pass' : 'fail',
|
|
517
|
+
severity: 'high',
|
|
518
|
+
detail: !parses ? 'MCP allowlist failed to parse' : hashes ? 'descriptor hashes match pins' : 'descriptor hash mismatch since pin',
|
|
519
|
+
fixable: false,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
// nightly (medium) — cron/timer registered + reachable.
|
|
523
|
+
{
|
|
524
|
+
const ok = deps.nightly.cronRegistered();
|
|
525
|
+
add({
|
|
526
|
+
id: 'nightly.cron',
|
|
527
|
+
domain: 'nightly',
|
|
528
|
+
status: ok ? 'pass' : 'fail',
|
|
529
|
+
severity: 'medium',
|
|
530
|
+
detail: ok ? 'consolidation timer registered' : 'consolidation timer not registered',
|
|
531
|
+
fixable: true,
|
|
532
|
+
fixId: 'nightly.register-cron',
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
// sidecars (medium) — Whisper model resolvable + ffmpeg on PATH.
|
|
536
|
+
{
|
|
537
|
+
const whisper = deps.whisperModelResolvable ? deps.whisperModelResolvable() : true;
|
|
538
|
+
const ffmpeg = deps.prereqs.version('ffmpeg') !== null;
|
|
539
|
+
const ok = whisper && ffmpeg;
|
|
540
|
+
add({
|
|
541
|
+
id: 'sidecars.media',
|
|
542
|
+
domain: 'sidecars',
|
|
543
|
+
status: ok ? 'pass' : 'fail',
|
|
544
|
+
severity: 'medium',
|
|
545
|
+
detail: !whisper ? 'Whisper model unresolvable' : !ffmpeg ? 'ffmpeg not on PATH' : 'Whisper + ffmpeg present',
|
|
546
|
+
fixable: false,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
// disk (medium) — free space above threshold.
|
|
550
|
+
{
|
|
551
|
+
const free = deps.diskFreeBytes ? deps.diskFreeBytes() : Number.MAX_SAFE_INTEGER;
|
|
552
|
+
const threshold = deps.diskThresholdBytes ?? 1024 * 1024 * 1024;
|
|
553
|
+
const ok = free >= threshold;
|
|
554
|
+
add({
|
|
555
|
+
id: 'disk.free-space',
|
|
556
|
+
domain: 'disk',
|
|
557
|
+
status: ok ? 'pass' : 'fail',
|
|
558
|
+
severity: 'medium',
|
|
559
|
+
detail: ok ? 'free space above threshold' : 'free space below threshold',
|
|
560
|
+
fixable: false,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
// clock (low) — sane + timezone resolvable, never literal "Auto".
|
|
564
|
+
{
|
|
565
|
+
const tz = deps.timezone ? deps.timezone() : 'UTC';
|
|
566
|
+
const ok = tz.length > 0 && tz !== 'Auto';
|
|
567
|
+
add({
|
|
568
|
+
id: 'clock.timezone',
|
|
569
|
+
domain: 'clock',
|
|
570
|
+
status: ok ? 'pass' : 'fail',
|
|
571
|
+
severity: 'low',
|
|
572
|
+
detail: ok ? `timezone=${tz}` : 'timezone unresolved (literal "Auto")',
|
|
573
|
+
fixable: false,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
// Post-upgrade subset: keep the checks that catch migration breakage
|
|
577
|
+
// (env schema drift, MCP descriptor-hash mismatch, provider id resolve).
|
|
578
|
+
let filtered = checks;
|
|
579
|
+
if (opts.postUpgrade) {
|
|
580
|
+
const postUpgradeDomains = new Set(['env', 'mcp', 'providers', 'memory']);
|
|
581
|
+
filtered = filtered.filter((c) => postUpgradeDomains.has(c.domain));
|
|
582
|
+
}
|
|
583
|
+
if (opts.only) {
|
|
584
|
+
const only = new Set(opts.only);
|
|
585
|
+
filtered = filtered.filter((c) => only.has(c.domain));
|
|
586
|
+
}
|
|
587
|
+
if (opts.skip) {
|
|
588
|
+
const skip = new Set(opts.skip);
|
|
589
|
+
filtered = filtered.filter((c) => !skip.has(c.domain));
|
|
590
|
+
}
|
|
591
|
+
// Deterministic order: sorted by stable check id (§4, AC-13-12).
|
|
592
|
+
return filtered.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
593
|
+
}
|
|
594
|
+
async function doctor(opts) {
|
|
595
|
+
const checks = await runChecks(opts);
|
|
596
|
+
// ok:false iff any high/critical check is "fail" (§3).
|
|
597
|
+
const ok = !checks.some((c) => c.status === 'fail' && (c.severity === 'high' || c.severity === 'critical'));
|
|
598
|
+
const report = { ok, ranAt: deps.clock.nowIso(), harnessVersion: deps.harnessVersion, checks };
|
|
599
|
+
emit('doctor.ran', { ok });
|
|
600
|
+
return report;
|
|
601
|
+
}
|
|
602
|
+
// -------------------------------------------------------------------------
|
|
603
|
+
// toJson — deterministic, secret-free serialization for `--json`.
|
|
604
|
+
// ranAt/harnessVersion are excluded so two runs over identical state diff
|
|
605
|
+
// byte-identically (AC-13-12); the check list is already id-sorted.
|
|
606
|
+
// -------------------------------------------------------------------------
|
|
607
|
+
function toJson(report) {
|
|
608
|
+
const stable = {
|
|
609
|
+
ok: report.ok,
|
|
610
|
+
checks: report.checks.map((c) => ({
|
|
611
|
+
id: c.id,
|
|
612
|
+
domain: c.domain,
|
|
613
|
+
status: c.status,
|
|
614
|
+
severity: c.severity,
|
|
615
|
+
detail: redact(c.detail),
|
|
616
|
+
fixable: c.fixable,
|
|
617
|
+
...(c.fixId !== undefined ? { fixId: c.fixId } : {}),
|
|
618
|
+
})),
|
|
619
|
+
};
|
|
620
|
+
return JSON.stringify(stable, null, 2) + '\n';
|
|
621
|
+
}
|
|
622
|
+
// -------------------------------------------------------------------------
|
|
623
|
+
// diagnostics — redacted support bundle (§4, AC-13-15/16). Every secret
|
|
624
|
+
// value is replaced with «redacted»; redactedFields lists every secret key.
|
|
625
|
+
// -------------------------------------------------------------------------
|
|
626
|
+
async function diagnostics(opts) {
|
|
627
|
+
const out = opts.out ?? 'aisy-diagnostics';
|
|
628
|
+
const values = secretValues();
|
|
629
|
+
const redactedFields = [...deps.vault.secretKeys()].sort();
|
|
630
|
+
// meta.json — versions + ranAt.
|
|
631
|
+
deps.fs.write(`${out}/meta.json`, JSON.stringify({ harnessVersion: deps.harnessVersion, ranAt: deps.clock.nowIso() }, null, 2));
|
|
632
|
+
// doctor.json — the report (already redaction-safe details).
|
|
633
|
+
const report = await doctor({});
|
|
634
|
+
deps.fs.write(`${out}/doctor.json`, toJson(report));
|
|
635
|
+
// config.redacted.json — resolved config with every secret value stripped.
|
|
636
|
+
const config = {};
|
|
637
|
+
for (const key of REQUIRED_ENV_KEYS) {
|
|
638
|
+
const isSecret = /KEY$|TOKEN$/.test(key);
|
|
639
|
+
config[key] = isSecret ? '«redacted»' : redactWith(values, envValueOf(key));
|
|
640
|
+
}
|
|
641
|
+
deps.fs.write(`${out}/config.redacted.json`, JSON.stringify(config, null, 2));
|
|
642
|
+
// journal.tail.jsonl — recent events, secret-redacted (spec 12 CSO-M3).
|
|
643
|
+
const rawTail = deps.fs.exists('journal.raw') ? deps.fs.read('journal.raw') : '';
|
|
644
|
+
deps.fs.write(`${out}/journal.tail.jsonl`, redactWith(values, rawTail));
|
|
645
|
+
emit('diagnostics.exported', { bundlePath: out });
|
|
646
|
+
return { bundlePath: out, redactedFields };
|
|
647
|
+
}
|
|
648
|
+
return { init, doctor, toJson, diagnostics };
|
|
649
|
+
}
|
|
650
|
+
// Templates ship with the harness; the operator owns the result (§4).
|
|
651
|
+
function templateFor(path) {
|
|
652
|
+
if (path === '.env') {
|
|
653
|
+
return REQUIRED_ENV_KEYS.map((k) => `${k}=`).join('\n') + '\n';
|
|
654
|
+
}
|
|
655
|
+
const name = path.split('/').pop() ?? path;
|
|
656
|
+
return `# ${name}\n\nScaffolded by aisy init. Edit this file directly.\n`;
|
|
657
|
+
}
|
|
658
|
+
// ===========================================================================
|
|
659
|
+
// makeInSessionCommands — /status /usage /context /doctor /consolidate (§5.4)
|
|
660
|
+
// Read-only commands never mutate; /consolidate only CONSTRUCTS+cards a
|
|
661
|
+
// PendingAction (the Gateway tap is the only thing that runs Nightly).
|
|
662
|
+
// ===========================================================================
|
|
663
|
+
export function makeInSessionCommands(deps) {
|
|
664
|
+
const emit = (event, payload) => deps.events?.emit(event, payload);
|
|
665
|
+
function bucketFor(period, events) {
|
|
666
|
+
if (period === 'turn') {
|
|
667
|
+
// The "turn" bucket is the single most recent charge.
|
|
668
|
+
return events.length > 0 ? [events[events.length - 1]] : [];
|
|
669
|
+
}
|
|
670
|
+
if (period === 'day') {
|
|
671
|
+
// "day" filters to charges dated on the current calendar day (UTC),
|
|
672
|
+
// sourced from the injected clock so it stays deterministic in tests.
|
|
673
|
+
const today = deps.clock.nowIso().slice(0, 10); // YYYY-MM-DD
|
|
674
|
+
return events.filter((e) => new Date(e.at).toISOString().slice(0, 10) === today);
|
|
675
|
+
}
|
|
676
|
+
// "session" aggregates the full in-session event list (the journal only
|
|
677
|
+
// holds the current session's events).
|
|
678
|
+
return events;
|
|
679
|
+
}
|
|
680
|
+
return {
|
|
681
|
+
async status() {
|
|
682
|
+
emit('command.invoked', { command: 'status' });
|
|
683
|
+
const events = deps.cost.chargedEvents();
|
|
684
|
+
const sessionCostUsd = events.reduce((s, e) => s + e.dollars, 0);
|
|
685
|
+
const lastTurnCostUsd = events.length > 0 ? events[events.length - 1].dollars : 0;
|
|
686
|
+
return {
|
|
687
|
+
routing: deps.cost.routing(),
|
|
688
|
+
contextFill: deps.cost.contextFill(),
|
|
689
|
+
lastTurnCostUsd,
|
|
690
|
+
sessionCostUsd,
|
|
691
|
+
};
|
|
692
|
+
},
|
|
693
|
+
async usage(period = 'session') {
|
|
694
|
+
emit('command.invoked', { command: 'usage' });
|
|
695
|
+
const events = bucketFor(period, deps.cost.chargedEvents());
|
|
696
|
+
const byTier = { reasoning: 0, critique: 0, routine: 0 };
|
|
697
|
+
let totalUsd = 0;
|
|
698
|
+
for (const e of events) {
|
|
699
|
+
byTier[e.tier] += e.dollars;
|
|
700
|
+
totalUsd += e.dollars;
|
|
701
|
+
}
|
|
702
|
+
return { period, byTier, totalUsd };
|
|
703
|
+
},
|
|
704
|
+
async context() {
|
|
705
|
+
emit('command.invoked', { command: 'context' });
|
|
706
|
+
// Only metadata (kind/name/size) is surfaced — never a secret or a full
|
|
707
|
+
// fact body (AC-13-21).
|
|
708
|
+
const items = deps.contextInventory.items().map((i) => ({ kind: i.kind, name: i.name, size: i.size }));
|
|
709
|
+
const totalSize = items.reduce((s, i) => s + i.size, 0);
|
|
710
|
+
return { items, totalSize };
|
|
711
|
+
},
|
|
712
|
+
async runDoctor() {
|
|
713
|
+
emit('command.invoked', { command: 'doctor' });
|
|
714
|
+
// Read-only by contract — doctor({}) performs no writes (AC-13-22).
|
|
715
|
+
return deps.ops.doctor({});
|
|
716
|
+
},
|
|
717
|
+
async requestConsolidate() {
|
|
718
|
+
emit('command.invoked', { command: 'consolidate' });
|
|
719
|
+
const lockHeld = deps.nightly.runLockHeld();
|
|
720
|
+
// Build the PendingAction; the Gateway cards it. Issuance is never a run
|
|
721
|
+
// (AC-13-23). While the lock is held, the summary signals reject/queue so
|
|
722
|
+
// a tap cannot start a second concurrent run (AC-13-24).
|
|
723
|
+
const summary = lockHeld
|
|
724
|
+
? 'A consolidation is already running (run-lock held) — request queued'
|
|
725
|
+
: 'Trigger a consolidation pass into the morning staging gate (not auto-promoted)';
|
|
726
|
+
const actionId = randomUUID();
|
|
727
|
+
const action = {
|
|
728
|
+
actionId,
|
|
729
|
+
actionHash: createHash('sha256').update(`consolidate:${actionId}`).digest('hex'),
|
|
730
|
+
tier: 2,
|
|
731
|
+
requiresStepUp: false,
|
|
732
|
+
summary,
|
|
733
|
+
};
|
|
734
|
+
await deps.card.issueCard(action);
|
|
735
|
+
return action;
|
|
736
|
+
},
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
export function makeBootstrapFlow(deps) {
|
|
740
|
+
const state = { started: false, completed: false, stepsDone: [] };
|
|
741
|
+
return {
|
|
742
|
+
async propose(span) {
|
|
743
|
+
// Setup runs strictly under operator provenance; an untrusted span never
|
|
744
|
+
// advances it and never even issues a card (AC-13-18; capability narrowing).
|
|
745
|
+
if (span.provenance !== 'operator')
|
|
746
|
+
return null;
|
|
747
|
+
const next = deps.steps.find((s) => !state.stepsDone.includes(s));
|
|
748
|
+
if (next === undefined)
|
|
749
|
+
return null;
|
|
750
|
+
if (!state.started) {
|
|
751
|
+
state.started = true;
|
|
752
|
+
deps.events?.emit('bootstrap.started');
|
|
753
|
+
}
|
|
754
|
+
// PROPOSE only: build a PendingAction and card it. Issuance is never
|
|
755
|
+
// confirmation — the operator's tap commits, exactly like any Tier-gated
|
|
756
|
+
// action (AC-13-17).
|
|
757
|
+
const actionId = randomUUID();
|
|
758
|
+
const action = {
|
|
759
|
+
actionId,
|
|
760
|
+
actionHash: createHash('sha256').update(`bootstrap:${next}:${actionId}`).digest('hex'),
|
|
761
|
+
tier: 2,
|
|
762
|
+
requiresStepUp: false,
|
|
763
|
+
summary: `BOOTSTRAP: configure "${next}"`,
|
|
764
|
+
};
|
|
765
|
+
const cardId = await deps.card.issueCard(action);
|
|
766
|
+
return { action, cardId };
|
|
767
|
+
},
|
|
768
|
+
// The ONLY setter of stepsDone — called by code on a confirmed card tap.
|
|
769
|
+
recordStepDone(stepId) {
|
|
770
|
+
if (!state.stepsDone.includes(stepId))
|
|
771
|
+
state.stepsDone.push(stepId);
|
|
772
|
+
},
|
|
773
|
+
// completed is set only by code, once all required steps are recorded
|
|
774
|
+
// (AC-13-17; the model can never self-complete setup).
|
|
775
|
+
markCompleteIfDone() {
|
|
776
|
+
const allDone = deps.steps.every((s) => state.stepsDone.includes(s));
|
|
777
|
+
if (allDone && !state.completed) {
|
|
778
|
+
state.completed = true;
|
|
779
|
+
deps.events?.emit('bootstrap.completed');
|
|
780
|
+
}
|
|
781
|
+
},
|
|
782
|
+
state() {
|
|
783
|
+
return { started: state.started, completed: state.completed, stepsDone: [...state.stepsDone] };
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
//# sourceMappingURL=index.js.map
|