@geravant/sinain 1.13.0 → 1.15.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/.env.example +33 -27
- package/cli.js +30 -14
- package/config-shared.js +173 -30
- package/launcher.js +38 -21
- package/onboard.js +36 -20
- package/package.json +4 -1
- package/sinain-agent/run.sh +600 -127
- package/sinain-core/src/agents-loader.ts +254 -0
- package/sinain-core/src/buffers/feed-buffer.ts +6 -4
- package/sinain-core/src/config.ts +77 -15
- package/sinain-core/src/escalation/escalator.ts +178 -18
- package/sinain-core/src/index.ts +218 -31
- package/sinain-core/src/learning/local-curation.ts +81 -27
- package/sinain-core/src/overlay/commands.ts +25 -0
- package/sinain-core/src/overlay/ws-handler.ts +3 -0
- package/sinain-core/src/server.ts +101 -10
- package/sinain-core/src/types.ts +29 -3
- package/sinain-memory/graph_query.py +12 -3
- package/sinain-memory/knowledge_integrator.py +194 -10
- package/sinain-memory/__pycache__/common.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/embed_client.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/graph_query.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/knowledge_integrator.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/session_distiller.cpython-312.pyc +0 -0
- package/sinain-memory/__pycache__/triplestore.cpython-312.pyc +0 -0
- package/sinain-memory/eval/__init__.py +0 -0
- package/sinain-memory/eval/__pycache__/__init__.cpython-312.pyc +0 -0
- package/sinain-memory/eval/assertions.py +0 -267
- package/sinain-memory/eval/benchmarks/__init__.py +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/__init__.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/base_adapter.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/config.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/evaluate.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/ingest.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/longmemeval_adapter.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/meeting_adapter.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/meeting_runner.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/query.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/report.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/__pycache__/runner.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/base_adapter.py +0 -43
- package/sinain-memory/eval/benchmarks/config.py +0 -23
- package/sinain-memory/eval/benchmarks/evaluate.py +0 -146
- package/sinain-memory/eval/benchmarks/ingest.py +0 -152
- package/sinain-memory/eval/benchmarks/judges/__init__.py +0 -0
- package/sinain-memory/eval/benchmarks/judges/__pycache__/__init__.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/judges/__pycache__/qa_judge.cpython-312.pyc +0 -0
- package/sinain-memory/eval/benchmarks/judges/qa_judge.py +0 -81
- package/sinain-memory/eval/benchmarks/longmemeval_adapter.py +0 -177
- package/sinain-memory/eval/benchmarks/meeting_adapter.py +0 -81
- package/sinain-memory/eval/benchmarks/meeting_runner.py +0 -230
- package/sinain-memory/eval/benchmarks/query.py +0 -193
- package/sinain-memory/eval/benchmarks/report.py +0 -87
- package/sinain-memory/eval/benchmarks/run_meeting_bench.sh +0 -318
- package/sinain-memory/eval/benchmarks/runner.py +0 -283
- package/sinain-memory/eval/judges/__init__.py +0 -0
- package/sinain-memory/eval/judges/base_judge.py +0 -61
- package/sinain-memory/eval/judges/curation_judge.py +0 -46
- package/sinain-memory/eval/judges/insight_judge.py +0 -48
- package/sinain-memory/eval/judges/mining_judge.py +0 -42
- package/sinain-memory/eval/judges/signal_judge.py +0 -45
- package/sinain-memory/eval/retrieval_benchmark.jsonl +0 -12
- package/sinain-memory/eval/retrieval_evaluator.py +0 -186
- package/sinain-memory/eval/schemas.py +0 -247
- package/sinain-memory/tests/__init__.py +0 -0
- package/sinain-memory/tests/conftest.py +0 -189
- package/sinain-memory/tests/test_curator_helpers.py +0 -94
- package/sinain-memory/tests/test_embedder.py +0 -210
- package/sinain-memory/tests/test_extract_json.py +0 -124
- package/sinain-memory/tests/test_feedback_computation.py +0 -121
- package/sinain-memory/tests/test_miner_helpers.py +0 -71
- package/sinain-memory/tests/test_module_management.py +0 -458
- package/sinain-memory/tests/test_parsers.py +0 -96
- package/sinain-memory/tests/test_tick_evaluator.py +0 -430
- package/sinain-memory/tests/test_triple_extractor.py +0 -255
- package/sinain-memory/tests/test_triple_ingest.py +0 -191
- package/sinain-memory/tests/test_triple_migrate.py +0 -138
- package/sinain-memory/tests/test_triplestore.py +0 -248
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
// agents-loader.ts
|
|
2
|
+
//
|
|
3
|
+
// sinain-core's view of sinain-agent/agents.json. Loaded once at startup,
|
|
4
|
+
// gives config.ts a typed snapshot of the bare-agent + OpenClaw config that
|
|
5
|
+
// used to live in .env.
|
|
6
|
+
//
|
|
7
|
+
// Why sinain-core reads it at all: the OpenClaw WS client is constructed
|
|
8
|
+
// before the bare agent ever registers, and stays connected across the
|
|
9
|
+
// session for heartbeat / situation push / feedback. So sinain-core needs
|
|
10
|
+
// the gateway URL/token at startup — it can't wait for run.sh to forward
|
|
11
|
+
// them via /bareagent/register.
|
|
12
|
+
//
|
|
13
|
+
// Path resolution:
|
|
14
|
+
// 1. AGENTS_CONFIG_PATH env var (explicit override)
|
|
15
|
+
// 2. <repo-root>/sinain-agent/agents.json (default)
|
|
16
|
+
// 3. <repo-root>/sinain-agent/agents.example.json (fresh-checkout fallback)
|
|
17
|
+
//
|
|
18
|
+
// Returns null if no candidate exists. config.ts treats null as "use env
|
|
19
|
+
// defaults" so the migration is non-breaking.
|
|
20
|
+
//
|
|
21
|
+
// Field expansion happens in two passes (shell-like ordering):
|
|
22
|
+
// 1. Cross-field literal expansion: ${allowedTools} inside escAllowedTools
|
|
23
|
+
// resolves against other top-level fields. Lets users compose
|
|
24
|
+
// whitelists by reference.
|
|
25
|
+
// 2. Env-var indirection: ${OPENROUTER_API_KEY}, ${HOME}, etc. resolve
|
|
26
|
+
// against process.env. Mirrors run.sh's apply_profile_env regex
|
|
27
|
+
// (\$\{[A-Za-z_][A-Za-z0-9_]*\}).
|
|
28
|
+
|
|
29
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
30
|
+
import { resolve, dirname } from "node:path";
|
|
31
|
+
import { fileURLToPath } from "node:url";
|
|
32
|
+
import { homedir } from "node:os";
|
|
33
|
+
|
|
34
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
35
|
+
|
|
36
|
+
// Regex shared with run.sh's apply_profile_env. Keep the two implementations
|
|
37
|
+
// in lockstep when changing either side.
|
|
38
|
+
const VAR_REF_RE = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
39
|
+
|
|
40
|
+
export interface OpenClawProfileFields {
|
|
41
|
+
type: "openclaw";
|
|
42
|
+
wsUrl?: string;
|
|
43
|
+
wsToken?: string;
|
|
44
|
+
httpUrl?: string;
|
|
45
|
+
httpToken?: string;
|
|
46
|
+
sessionKey?: string;
|
|
47
|
+
phase1TimeoutMs?: number;
|
|
48
|
+
phase2TimeoutMs?: number;
|
|
49
|
+
pingIntervalMs?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface AgentProfile {
|
|
53
|
+
type?: string;
|
|
54
|
+
bin?: string;
|
|
55
|
+
settings?: string;
|
|
56
|
+
model?: string;
|
|
57
|
+
env?: Record<string, string>;
|
|
58
|
+
// openclaw-specific fields (typed loose since profiles are heterogeneous)
|
|
59
|
+
wsUrl?: string;
|
|
60
|
+
wsToken?: string;
|
|
61
|
+
httpUrl?: string;
|
|
62
|
+
httpToken?: string;
|
|
63
|
+
sessionKey?: string;
|
|
64
|
+
phase1TimeoutMs?: number;
|
|
65
|
+
phase2TimeoutMs?: number;
|
|
66
|
+
pingIntervalMs?: number;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface AnalyzerBlock {
|
|
70
|
+
debounceMs?: number;
|
|
71
|
+
maxIntervalMs?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface EscalationBlock {
|
|
75
|
+
mode?: string;
|
|
76
|
+
cooldownMs?: number;
|
|
77
|
+
staleMs?: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface AgentsConfig {
|
|
81
|
+
default?: string;
|
|
82
|
+
pollIntervalSec?: number;
|
|
83
|
+
agentMaxTurns?: number;
|
|
84
|
+
spawnMaxTurns?: number;
|
|
85
|
+
allowedTools?: string;
|
|
86
|
+
escAllowedTools?: string;
|
|
87
|
+
spawnAllowedTools?: string;
|
|
88
|
+
autoApproveTools?: string;
|
|
89
|
+
analyzer?: AnalyzerBlock;
|
|
90
|
+
escalation?: EscalationBlock;
|
|
91
|
+
profiles?: Record<string, AgentProfile>;
|
|
92
|
+
/** Absolute path of the file actually loaded (for debug logging). */
|
|
93
|
+
loadedFrom?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function expandVars(value: string): string {
|
|
97
|
+
return value.replace(VAR_REF_RE, (_, name: string) => process.env[name] ?? "");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Recursively expand ${env} references in any string field of `obj`. */
|
|
101
|
+
function expandEnvDeep<T>(obj: T): T {
|
|
102
|
+
if (obj === null || obj === undefined) return obj;
|
|
103
|
+
if (typeof obj === "string") return expandVars(obj) as unknown as T;
|
|
104
|
+
if (Array.isArray(obj)) return obj.map((x) => expandEnvDeep(x)) as unknown as T;
|
|
105
|
+
if (typeof obj === "object") {
|
|
106
|
+
const out: Record<string, unknown> = {};
|
|
107
|
+
for (const [k, v] of Object.entries(obj as Record<string, unknown>)) {
|
|
108
|
+
out[k] = expandEnvDeep(v);
|
|
109
|
+
}
|
|
110
|
+
return out as T;
|
|
111
|
+
}
|
|
112
|
+
return obj;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Cross-field literal expansion at the top level. Lets users write
|
|
117
|
+
* "escAllowedTools": "${allowedTools} Bash(git:*) Edit"
|
|
118
|
+
* and have it compose with the value of `allowedTools`. Only string
|
|
119
|
+
* top-level fields are eligible to be referenced; nested blocks aren't.
|
|
120
|
+
*/
|
|
121
|
+
function expandTopLevelRefs(cfg: AgentsConfig): void {
|
|
122
|
+
const refTargets: Record<string, string | undefined> = {
|
|
123
|
+
allowedTools: cfg.allowedTools,
|
|
124
|
+
escAllowedTools: cfg.escAllowedTools,
|
|
125
|
+
spawnAllowedTools: cfg.spawnAllowedTools,
|
|
126
|
+
autoApproveTools: cfg.autoApproveTools,
|
|
127
|
+
default: cfg.default,
|
|
128
|
+
};
|
|
129
|
+
const expand = (s?: string): string | undefined => {
|
|
130
|
+
if (!s) return s;
|
|
131
|
+
return s.replace(VAR_REF_RE, (m, name: string) => {
|
|
132
|
+
const ref = refTargets[name];
|
|
133
|
+
// Only substitute when the referenced field is a top-level field
|
|
134
|
+
// *that has a value*. Otherwise leave the placeholder untouched so
|
|
135
|
+
// the env-var pass can still pick it up (e.g. ${HOME}).
|
|
136
|
+
return ref !== undefined ? ref : m;
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
cfg.escAllowedTools = expand(cfg.escAllowedTools);
|
|
140
|
+
cfg.spawnAllowedTools = expand(cfg.spawnAllowedTools);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function candidatePaths(): string[] {
|
|
144
|
+
const explicit = process.env.AGENTS_CONFIG_PATH;
|
|
145
|
+
if (explicit) return [resolve(explicit)];
|
|
146
|
+
// ~/.sinain/agents.json is the wizard's write target — works on npm
|
|
147
|
+
// installs where the package dir is read-only. Highest priority after
|
|
148
|
+
// an explicit env override so user-home config wins over the shipped
|
|
149
|
+
// template.
|
|
150
|
+
const userHome = resolve(homedir(), ".sinain", "agents.json");
|
|
151
|
+
// sinain-core compiled output sits at sinain-core/dist/, source at
|
|
152
|
+
// sinain-core/src/. From either, sinain-agent is two levels up.
|
|
153
|
+
const repoRoot = resolve(__dirname, "..", "..");
|
|
154
|
+
return [
|
|
155
|
+
userHome,
|
|
156
|
+
resolve(repoRoot, "sinain-agent", "agents.json"),
|
|
157
|
+
resolve(repoRoot, "sinain-agent", "agents.example.json"),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Load agents.json (or fall back to agents.example.json). Returns null if
|
|
163
|
+
* neither candidate file exists or parsing fails. Caller (config.ts) is
|
|
164
|
+
* expected to treat null as "use env defaults" so the migration is
|
|
165
|
+
* non-breaking.
|
|
166
|
+
*/
|
|
167
|
+
export function loadAgentsConfig(): AgentsConfig | null {
|
|
168
|
+
for (const path of candidatePaths()) {
|
|
169
|
+
if (!existsSync(path)) continue;
|
|
170
|
+
try {
|
|
171
|
+
const raw = readFileSync(path, "utf-8");
|
|
172
|
+
const parsed = JSON.parse(raw) as AgentsConfig;
|
|
173
|
+
// Pass 1: cross-field literal substitution at top level. Has to run
|
|
174
|
+
// before env-var expansion so `${allowedTools}` doesn't accidentally
|
|
175
|
+
// resolve to process.env.allowedTools (which doesn't exist anyway,
|
|
176
|
+
// but the principle of layered substitution is what we want).
|
|
177
|
+
expandTopLevelRefs(parsed);
|
|
178
|
+
// Pass 2: ${ENV_VAR} expansion across the whole tree. Profile-level
|
|
179
|
+
// env blocks are deliberately skipped here — those are applied by
|
|
180
|
+
// run.sh in a subshell at invocation time, not by sinain-core.
|
|
181
|
+
const profiles = parsed.profiles;
|
|
182
|
+
delete parsed.profiles;
|
|
183
|
+
const expanded = expandEnvDeep(parsed);
|
|
184
|
+
// Restore profiles (with their own env blocks expanded for the few
|
|
185
|
+
// openclaw-specific fields that sinain-core does read).
|
|
186
|
+
if (profiles) {
|
|
187
|
+
const expandedProfiles: Record<string, AgentProfile> = {};
|
|
188
|
+
for (const [name, profile] of Object.entries(profiles)) {
|
|
189
|
+
// Don't expand the per-profile `env` block — those are evaluated
|
|
190
|
+
// at agent invocation time inside run.sh. But DO expand other
|
|
191
|
+
// profile fields (wsToken, etc.) since sinain-core reads them.
|
|
192
|
+
const { env: profileEnv, ...rest } = profile;
|
|
193
|
+
expandedProfiles[name] = {
|
|
194
|
+
...expandEnvDeep(rest),
|
|
195
|
+
...(profileEnv !== undefined ? { env: profileEnv } : {}),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
expanded.profiles = expandedProfiles;
|
|
199
|
+
}
|
|
200
|
+
expanded.loadedFrom = path;
|
|
201
|
+
return expanded;
|
|
202
|
+
} catch (err) {
|
|
203
|
+
console.warn(`[agents-loader] failed to parse ${path}: ${(err as Error).message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Convenience: pull the openclaw profile if present, else null.
|
|
210
|
+
* @deprecated Prefer findGatewayProfile() — it doesn't assume the name. */
|
|
211
|
+
export function getOpenClawProfile(cfg: AgentsConfig | null): AgentProfile | null {
|
|
212
|
+
return cfg?.profiles?.openclaw ?? null;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Find a gateway-style profile (any profile with `type: "openclaw"`,
|
|
217
|
+
* regardless of the profile's name). Used by sinain-core to:
|
|
218
|
+
* - load WS connection params at startup (config.ts)
|
|
219
|
+
* - decide whether a lane choice should route via WS or HTTP (escalator.ts)
|
|
220
|
+
*
|
|
221
|
+
* Preference order: the canonical name "openclaw" wins if present, otherwise
|
|
222
|
+
* the first openclaw-typed profile found in the JSON. This lets users add
|
|
223
|
+
* `nemoclaw`, `nanoclaw-prod`, etc. with their own URLs as drop-in replacements.
|
|
224
|
+
*/
|
|
225
|
+
export function findGatewayProfile(
|
|
226
|
+
cfg: AgentsConfig | null,
|
|
227
|
+
): { name: string; profile: AgentProfile } | null {
|
|
228
|
+
const profiles = cfg?.profiles;
|
|
229
|
+
if (!profiles) return null;
|
|
230
|
+
if (profiles.openclaw?.type === "openclaw") {
|
|
231
|
+
return { name: "openclaw", profile: profiles.openclaw };
|
|
232
|
+
}
|
|
233
|
+
for (const [name, profile] of Object.entries(profiles)) {
|
|
234
|
+
if (profile?.type === "openclaw") return { name, profile };
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Is the given profile name a gateway-style (WS-dispatched) profile? */
|
|
240
|
+
export function isGatewayProfile(
|
|
241
|
+
cfg: AgentsConfig | null,
|
|
242
|
+
name: string,
|
|
243
|
+
): boolean {
|
|
244
|
+
return cfg?.profiles?.[name]?.type === "openclaw";
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** All openclaw-typed profile names in the config. */
|
|
248
|
+
export function gatewayProfileNames(cfg: AgentsConfig | null): string[] {
|
|
249
|
+
const profiles = cfg?.profiles;
|
|
250
|
+
if (!profiles) return [];
|
|
251
|
+
return Object.entries(profiles)
|
|
252
|
+
.filter(([_, p]) => p?.type === "openclaw")
|
|
253
|
+
.map(([name]) => name);
|
|
254
|
+
}
|
|
@@ -48,12 +48,14 @@ export class FeedBuffer {
|
|
|
48
48
|
this.items.push(item);
|
|
49
49
|
if (this.items.length > this._hwm) this._hwm = this.items.length;
|
|
50
50
|
|
|
51
|
-
// Fire
|
|
52
|
-
//
|
|
51
|
+
// Fire when enough new items have arrived since last distillation.
|
|
52
|
+
// 20 items ≈ 1.7 min of audio at ~12 items/min transcription rate.
|
|
53
|
+
// Distillation takes ~7s, so 20-item threshold gives 100s gap — safe margin.
|
|
54
|
+
// This means ~35 passes/hour, leaving <20 items undistilled at shutdown.
|
|
53
55
|
const newSinceRearm = this._version - this._onFullVersion;
|
|
54
|
-
if (this.items.length >=
|
|
56
|
+
if (this.items.length >= 20
|
|
55
57
|
&& this._onFullCb && this._onFullArmed
|
|
56
|
-
&& newSinceRearm >=
|
|
58
|
+
&& newSinceRearm >= 20) {
|
|
57
59
|
this._onFullArmed = false;
|
|
58
60
|
const snapshot = [...this.items];
|
|
59
61
|
queueMicrotask(() => this._onFullCb!(snapshot));
|
|
@@ -2,8 +2,9 @@ import { readFileSync, existsSync } from "node:fs";
|
|
|
2
2
|
import { resolve, dirname } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import os from "node:os";
|
|
5
|
-
import type { CoreConfig, AudioPipelineConfig, TranscriptionConfig, AnalysisConfig, EscalationConfig, OpenClawConfig, EscalationMode,
|
|
5
|
+
import type { CoreConfig, AudioPipelineConfig, TranscriptionConfig, AnalysisConfig, EscalationConfig, OpenClawConfig, EscalationMode, LearningConfig, PrivacyConfig, PrivacyMatrix, PrivacyLevel, PrivacyRow } from "./types.js";
|
|
6
6
|
import { PRESETS } from "./privacy/presets.js";
|
|
7
|
+
import { loadAgentsConfig, findGatewayProfile, type AgentsConfig } from "./agents-loader.js";
|
|
7
8
|
|
|
8
9
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
10
|
|
|
@@ -140,6 +141,26 @@ function loadPrivacyConfig(): PrivacyConfig {
|
|
|
140
141
|
}
|
|
141
142
|
|
|
142
143
|
export function loadConfig(): CoreConfig {
|
|
144
|
+
// sinain-agent/agents.json is the new single source of truth for the
|
|
145
|
+
// bare-agent + OpenClaw config that used to live in .env. Loading
|
|
146
|
+
// happens once at startup; null means the file isn't there (fresh
|
|
147
|
+
// checkout, custom layout, etc.) and we fall back to env defaults.
|
|
148
|
+
const agentsCfg = loadAgentsConfig();
|
|
149
|
+
if (agentsCfg) {
|
|
150
|
+
console.log(`[config] loaded agents config from ${agentsCfg.loadedFrom}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Helpers that prefer agents.json values, then env, then a hardcoded
|
|
154
|
+
// default. Keeps .env working as a safety net during the migration.
|
|
155
|
+
const fromCfgStr = (cfgVal: string | undefined, envKey: string, def: string): string => {
|
|
156
|
+
if (cfgVal !== undefined && cfgVal !== "") return cfgVal;
|
|
157
|
+
return env(envKey, def);
|
|
158
|
+
};
|
|
159
|
+
const fromCfgInt = (cfgVal: number | undefined, envKey: string, def: number): number => {
|
|
160
|
+
if (cfgVal !== undefined) return cfgVal;
|
|
161
|
+
return intEnv(envKey, def);
|
|
162
|
+
};
|
|
163
|
+
|
|
143
164
|
const audioConfig: AudioPipelineConfig = {
|
|
144
165
|
device: env("AUDIO_DEVICE", "BlackHole 2ch"),
|
|
145
166
|
sampleRate: intEnv("AUDIO_SAMPLE_RATE", 16000),
|
|
@@ -196,30 +217,57 @@ export function loadConfig(): CoreConfig {
|
|
|
196
217
|
.split(",").map(s => s.trim()).filter(Boolean),
|
|
197
218
|
timeout: intEnv("ANALYSIS_TIMEOUT", 15000),
|
|
198
219
|
pushToFeed: boolEnv("AGENT_PUSH_TO_FEED", true),
|
|
199
|
-
|
|
200
|
-
|
|
220
|
+
// analyzer pacing: agents.json `analyzer` block, fall back to env
|
|
221
|
+
debounceMs: fromCfgInt(agentsCfg?.analyzer?.debounceMs, "AGENT_DEBOUNCE_MS", 3000),
|
|
222
|
+
maxIntervalMs: fromCfgInt(agentsCfg?.analyzer?.maxIntervalMs, "AGENT_MAX_INTERVAL_MS", 30000),
|
|
201
223
|
cooldownMs: intEnv("AGENT_COOLDOWN_MS", 10000),
|
|
202
224
|
maxAgeMs: intEnv("AGENT_MAX_AGE_MS", 120000),
|
|
203
225
|
historyLimit: intEnv("AGENT_HISTORY_LIMIT", 50),
|
|
204
226
|
};
|
|
205
227
|
|
|
206
|
-
|
|
228
|
+
// escalation policy: agents.json `escalation` block, fall back to env.
|
|
229
|
+
// Mode is runtime-mutable via the overlay's flash-icon selector; this only
|
|
230
|
+
// sets the boot-time default. (Transport is no longer a setting — per-lane
|
|
231
|
+
// agent choice in the overlay determines WS vs HTTP dispatch.)
|
|
232
|
+
const escalationMode = fromCfgStr(agentsCfg?.escalation?.mode, "ESCALATION_MODE", "rich") as EscalationMode;
|
|
207
233
|
const escalationConfig: EscalationConfig = {
|
|
208
234
|
mode: escalationMode,
|
|
209
|
-
cooldownMs:
|
|
210
|
-
staleMs:
|
|
211
|
-
transport: env("ESCALATION_TRANSPORT", "auto") as EscalationTransport,
|
|
235
|
+
cooldownMs: fromCfgInt(agentsCfg?.escalation?.cooldownMs, "ESCALATION_COOLDOWN_MS", 30000),
|
|
236
|
+
staleMs: fromCfgInt(agentsCfg?.escalation?.staleMs, "ESCALATION_STALE_MS", 90000),
|
|
212
237
|
};
|
|
213
238
|
|
|
239
|
+
// OpenClaw gateway config: pick the first openclaw-TYPED profile
|
|
240
|
+
// (preferring the canonical name "openclaw"). This lets users add
|
|
241
|
+
// server-side variants — `nemoclaw`, `nanoclaw-prod`, etc. — as full
|
|
242
|
+
// peers; the WS client connects to whichever shows up first in the
|
|
243
|
+
// profiles list. Removing all openclaw-typed profiles (and unsetting
|
|
244
|
+
// the env vars) leaves gatewayWsUrl empty → registerBareAgent skips
|
|
245
|
+
// injecting any gateway entry into the roster, and escalator.start()
|
|
246
|
+
// skips the WS connect.
|
|
247
|
+
//
|
|
248
|
+
// Limitation: only ONE gateway URL gets a live WS client today. Adding
|
|
249
|
+
// multiple openclaw-typed profiles with different URLs lets the user
|
|
250
|
+
// pick them in the overlay roster, but dispatch always uses the first
|
|
251
|
+
// one's connection params. True per-profile WS clients is a follow-up.
|
|
252
|
+
const gateway = findGatewayProfile(agentsCfg);
|
|
253
|
+
const openclawProfile = gateway?.profile;
|
|
254
|
+
// wsUrl/hookUrl default to empty so "delete the openclaw profile" genuinely
|
|
255
|
+
// disables the gateway. The shipped agents.example.json includes the
|
|
256
|
+
// profile with localhost defaults, so fresh installs still work out of
|
|
257
|
+
// the box; removal is an explicit opt-out.
|
|
214
258
|
const openclawConfig: OpenClawConfig = {
|
|
215
|
-
gatewayWsUrl:
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
259
|
+
gatewayWsUrl: fromCfgStr(openclawProfile?.wsUrl, "OPENCLAW_WS_URL",
|
|
260
|
+
envAllowEmpty("OPENCLAW_GATEWAY_WS_URL", undefined, "")),
|
|
261
|
+
gatewayToken: fromCfgStr(openclawProfile?.wsToken, "OPENCLAW_WS_TOKEN",
|
|
262
|
+
env("OPENCLAW_GATEWAY_TOKEN", "")),
|
|
263
|
+
hookUrl: fromCfgStr(openclawProfile?.httpUrl, "OPENCLAW_HTTP_URL",
|
|
264
|
+
envAllowEmpty("OPENCLAW_HOOK_URL", undefined, "")),
|
|
265
|
+
hookToken: fromCfgStr(openclawProfile?.httpToken, "OPENCLAW_HTTP_TOKEN",
|
|
266
|
+
env("OPENCLAW_HOOK_TOKEN", "")),
|
|
267
|
+
sessionKey: fromCfgStr(openclawProfile?.sessionKey, "OPENCLAW_SESSION_KEY", "agent:main:sinain"),
|
|
268
|
+
phase1TimeoutMs: fromCfgInt(openclawProfile?.phase1TimeoutMs, "OPENCLAW_PHASE1_TIMEOUT_MS", 30_000),
|
|
269
|
+
phase2TimeoutMs: fromCfgInt(openclawProfile?.phase2TimeoutMs, "OPENCLAW_PHASE2_TIMEOUT_MS", 120_000),
|
|
270
|
+
pingIntervalMs: fromCfgInt(openclawProfile?.pingIntervalMs, "OPENCLAW_PING_INTERVAL_MS", 30_000),
|
|
223
271
|
};
|
|
224
272
|
|
|
225
273
|
const situationDir = env("OPENCLAW_WORKSPACE_DIR", "~/.openclaw/workspace");
|
|
@@ -236,6 +284,19 @@ export function loadConfig(): CoreConfig {
|
|
|
236
284
|
|
|
237
285
|
const privacyConfig = loadPrivacyConfig();
|
|
238
286
|
|
|
287
|
+
// autoApproveTools: agents.json top-level field, falls back to
|
|
288
|
+
// SINAIN_AUTO_APPROVE_TOOLS env. Space-separated tool names or prefix
|
|
289
|
+
// patterns (suffix *) that sinain-core auto-approves at /spawn/approve
|
|
290
|
+
// without routing to the overlay.
|
|
291
|
+
const autoApproveRaw = fromCfgStr(
|
|
292
|
+
agentsCfg?.autoApproveTools,
|
|
293
|
+
"SINAIN_AUTO_APPROVE_TOOLS",
|
|
294
|
+
"Read Glob Grep Ls Cat mcp__sinain*",
|
|
295
|
+
);
|
|
296
|
+
const permissionsConfig = {
|
|
297
|
+
autoApproveTools: autoApproveRaw.split(/\s+/).filter((t) => t.length > 0),
|
|
298
|
+
};
|
|
299
|
+
|
|
239
300
|
return {
|
|
240
301
|
port: intEnv("PORT", 9500),
|
|
241
302
|
audioConfig,
|
|
@@ -251,5 +312,6 @@ export function loadConfig(): CoreConfig {
|
|
|
251
312
|
costDisplayEnabled: boolEnv("COST_DISPLAY_ENABLED", false),
|
|
252
313
|
learningConfig,
|
|
253
314
|
privacyConfig,
|
|
315
|
+
permissionsConfig,
|
|
254
316
|
};
|
|
255
317
|
}
|