nemoris 0.1.0 → 0.1.2
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 +49 -49
- package/LICENSE +21 -21
- package/README.md +209 -209
- package/SECURITY.md +59 -119
- package/bin/nemoris +46 -46
- package/config/agents/agent.toml.example +28 -28
- package/config/agents/content.toml +23 -0
- package/config/agents/default.toml +22 -22
- package/config/agents/heartbeat.toml +35 -0
- package/config/agents/iris.toml +23 -0
- package/config/agents/lab.toml +23 -0
- package/config/agents/main.toml +45 -0
- package/config/agents/nemo.toml +21 -0
- package/config/agents/ops.toml +38 -0
- package/config/agents/orchestrator.toml +18 -18
- package/config/agents/revenue.toml +23 -0
- package/config/agents/testyboo.toml +19 -0
- package/config/delivery.toml +73 -73
- package/config/embeddings.toml +5 -5
- package/config/identity/content-purpose.md +11 -0
- package/config/identity/content-soul.md +45 -0
- package/config/identity/default-purpose.md +1 -1
- package/config/identity/default-soul.md +3 -3
- package/config/identity/heartbeat-purpose.md +9 -0
- package/config/identity/heartbeat-soul.md +16 -0
- package/config/identity/iris-purpose.md +17 -0
- package/config/identity/iris-soul.md +68 -0
- package/config/identity/lab-purpose.md +10 -0
- package/config/identity/lab-soul.md +38 -0
- package/config/identity/main-purpose.md +17 -0
- package/config/identity/main-soul.md +66 -0
- package/config/identity/main-user.md +22 -0
- package/config/identity/ops-purpose.md +9 -0
- package/config/identity/ops-soul.md +16 -0
- package/config/identity/orchestrator-purpose.md +1 -1
- package/config/identity/orchestrator-soul.md +1 -1
- package/config/identity/revenue-purpose.md +9 -0
- package/config/identity/revenue-soul.md +41 -0
- package/config/identity/testyboo-purpose.md +13 -0
- package/config/identity/testyboo-soul.md +20 -0
- package/config/improvement-targets.toml +15 -15
- package/config/jobs/heartbeat-check.toml +30 -30
- package/config/jobs/memory-rollup.toml +46 -46
- package/config/jobs/workspace-health.toml +63 -63
- package/config/mcp.toml +16 -16
- package/config/output-contracts.toml +17 -17
- package/config/peers.toml +32 -32
- package/config/peers.toml.example +32 -32
- package/config/policies/memory-default.toml +10 -10
- package/config/policies/memory-heartbeat.toml +5 -5
- package/config/policies/memory-ops.toml +10 -10
- package/config/policies/tools-heartbeat-minimal.toml +8 -8
- package/config/policies/tools-interactive-safe.toml +8 -8
- package/config/policies/tools-ops-bounded.toml +8 -8
- package/config/policies/tools-orchestrator.toml +7 -7
- package/config/providers/anthropic.toml +15 -15
- package/config/providers/ollama.toml +5 -5
- package/config/providers/openai-codex.toml +9 -9
- package/config/providers/openrouter.toml +5 -5
- package/config/router.toml +22 -22
- package/config/runtime.toml +114 -114
- package/config/skills/self-improvement.toml +15 -15
- package/config/skills/telegram-onboarding-spec.md +240 -240
- package/config/skills/workspace-monitor.toml +15 -15
- package/config/task-router.toml +42 -42
- package/install.sh +50 -50
- package/package.json +91 -90
- package/src/auth/auth-profiles.js +169 -169
- package/src/auth/openai-codex-oauth.js +285 -285
- package/src/battle.js +449 -449
- package/src/cli/help.js +265 -265
- package/src/cli/output-filter.js +49 -49
- package/src/cli/runtime-control.js +704 -704
- package/src/cli-main.js +2763 -2763
- package/src/cli.js +78 -78
- package/src/config/loader.js +332 -332
- package/src/config/schema-validator.js +214 -214
- package/src/config/toml-lite.js +8 -8
- package/src/daemon/action-handlers.js +71 -71
- package/src/daemon/healing-tick.js +87 -87
- package/src/daemon/health-probes.js +90 -90
- package/src/daemon/notifier.js +57 -57
- package/src/daemon/nurse.js +218 -218
- package/src/daemon/repair-log.js +106 -106
- package/src/daemon/rule-staging.js +90 -90
- package/src/daemon/rules.js +29 -29
- package/src/daemon/telegram-commands.js +54 -54
- package/src/daemon/updater.js +85 -85
- package/src/jobs/job-runner.js +78 -78
- package/src/mcp/consumer.js +129 -129
- package/src/memory/active-recall.js +171 -171
- package/src/memory/backend-manager.js +97 -97
- package/src/memory/backends/file-backend.js +38 -38
- package/src/memory/backends/qmd-backend.js +219 -219
- package/src/memory/embedding-guards.js +24 -24
- package/src/memory/embedding-index.js +118 -118
- package/src/memory/embedding-service.js +179 -179
- package/src/memory/file-index.js +177 -177
- package/src/memory/memory-signature.js +5 -5
- package/src/memory/memory-store.js +648 -648
- package/src/memory/retrieval-planner.js +66 -66
- package/src/memory/scoring.js +145 -145
- package/src/memory/simhash.js +78 -78
- package/src/memory/sqlite-active-store.js +824 -824
- package/src/memory/write-policy.js +36 -36
- package/src/onboarding/aliases.js +33 -33
- package/src/onboarding/auth/api-key.js +224 -224
- package/src/onboarding/auth/ollama-detect.js +42 -42
- package/src/onboarding/clack-prompter.js +77 -77
- package/src/onboarding/doctor.js +530 -530
- package/src/onboarding/lock.js +42 -42
- package/src/onboarding/model-catalog.js +344 -344
- package/src/onboarding/phases/auth.js +576 -589
- package/src/onboarding/phases/build.js +130 -130
- package/src/onboarding/phases/choose.js +82 -82
- package/src/onboarding/phases/detect.js +98 -98
- package/src/onboarding/phases/hatch.js +216 -216
- package/src/onboarding/phases/identity.js +79 -79
- package/src/onboarding/phases/ollama.js +345 -345
- package/src/onboarding/phases/scaffold.js +99 -99
- package/src/onboarding/phases/telegram.js +377 -377
- package/src/onboarding/phases/validate.js +204 -204
- package/src/onboarding/phases/verify.js +206 -206
- package/src/onboarding/platform.js +482 -482
- package/src/onboarding/status-bar.js +95 -95
- package/src/onboarding/templates.js +794 -794
- package/src/onboarding/toml-writer.js +38 -38
- package/src/onboarding/tui.js +250 -250
- package/src/onboarding/uninstall.js +153 -153
- package/src/onboarding/wizard.js +516 -499
- package/src/providers/anthropic.js +168 -168
- package/src/providers/base.js +247 -247
- package/src/providers/circuit-breaker.js +136 -136
- package/src/providers/ollama.js +163 -163
- package/src/providers/openai-codex.js +149 -149
- package/src/providers/openrouter.js +136 -136
- package/src/providers/registry.js +36 -36
- package/src/providers/router.js +16 -16
- package/src/runtime/bootstrap-cache.js +47 -47
- package/src/runtime/capabilities-prompt.js +25 -25
- package/src/runtime/completion-ping.js +99 -99
- package/src/runtime/config-validator.js +121 -121
- package/src/runtime/context-ledger.js +360 -360
- package/src/runtime/cutover-readiness.js +42 -42
- package/src/runtime/daemon.js +729 -729
- package/src/runtime/delivery-ack.js +195 -195
- package/src/runtime/delivery-adapters/local-file.js +41 -41
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
- package/src/runtime/delivery-adapters/shadow.js +13 -13
- package/src/runtime/delivery-adapters/standalone-http.js +98 -98
- package/src/runtime/delivery-adapters/telegram.js +104 -104
- package/src/runtime/delivery-adapters/tui.js +128 -128
- package/src/runtime/delivery-manager.js +807 -807
- package/src/runtime/delivery-store.js +168 -168
- package/src/runtime/dependency-health.js +118 -118
- package/src/runtime/envelope.js +114 -114
- package/src/runtime/evaluation.js +1089 -1089
- package/src/runtime/exec-approvals.js +216 -216
- package/src/runtime/executor.js +500 -500
- package/src/runtime/failure-ping.js +67 -67
- package/src/runtime/flows.js +83 -83
- package/src/runtime/guards.js +45 -45
- package/src/runtime/handoff.js +51 -51
- package/src/runtime/identity-cache.js +28 -28
- package/src/runtime/improvement-engine.js +109 -109
- package/src/runtime/improvement-harness.js +581 -581
- package/src/runtime/input-sanitiser.js +72 -72
- package/src/runtime/interaction-contract.js +347 -347
- package/src/runtime/lane-readiness.js +226 -226
- package/src/runtime/migration.js +323 -323
- package/src/runtime/model-resolution.js +78 -78
- package/src/runtime/network.js +64 -64
- package/src/runtime/notification-store.js +97 -97
- package/src/runtime/notifier.js +256 -256
- package/src/runtime/orchestrator.js +53 -53
- package/src/runtime/orphan-reaper.js +41 -41
- package/src/runtime/output-contract-schema.js +139 -139
- package/src/runtime/output-contract-validator.js +439 -439
- package/src/runtime/peer-readiness.js +69 -69
- package/src/runtime/peer-registry.js +133 -133
- package/src/runtime/pilot-status.js +108 -108
- package/src/runtime/prompt-builder.js +261 -261
- package/src/runtime/provider-attempt.js +582 -582
- package/src/runtime/report-fallback.js +71 -71
- package/src/runtime/result-normalizer.js +183 -183
- package/src/runtime/retention.js +74 -74
- package/src/runtime/review.js +244 -244
- package/src/runtime/route-job.js +15 -15
- package/src/runtime/run-store.js +38 -38
- package/src/runtime/schedule.js +88 -88
- package/src/runtime/scheduler-state.js +434 -434
- package/src/runtime/scheduler.js +656 -656
- package/src/runtime/session-compactor.js +182 -182
- package/src/runtime/session-search.js +155 -155
- package/src/runtime/slack-inbound.js +249 -249
- package/src/runtime/ssrf.js +102 -102
- package/src/runtime/status-aggregator.js +330 -330
- package/src/runtime/task-contract.js +140 -140
- package/src/runtime/task-packet.js +107 -107
- package/src/runtime/task-router.js +140 -140
- package/src/runtime/telegram-inbound.js +1565 -1565
- package/src/runtime/token-counter.js +134 -134
- package/src/runtime/token-estimator.js +59 -59
- package/src/runtime/tool-loop.js +200 -200
- package/src/runtime/transport-server.js +311 -311
- package/src/runtime/tui-server.js +411 -411
- package/src/runtime/ulid.js +44 -44
- package/src/security/ssrf-check.js +197 -197
- package/src/setup.js +369 -369
- package/src/shadow/bridge.js +303 -303
- package/src/skills/loader.js +84 -84
- package/src/tools/catalog.json +49 -49
- package/src/tools/cli-delegate.js +44 -44
- package/src/tools/mcp-client.js +106 -106
- package/src/tools/micro/cancel-task.js +6 -6
- package/src/tools/micro/complete-task.js +6 -6
- package/src/tools/micro/fail-task.js +6 -6
- package/src/tools/micro/http-fetch.js +74 -74
- package/src/tools/micro/index.js +36 -36
- package/src/tools/micro/lcm-recall.js +60 -60
- package/src/tools/micro/list-dir.js +17 -17
- package/src/tools/micro/list-skills.js +46 -46
- package/src/tools/micro/load-skill.js +38 -38
- package/src/tools/micro/memory-search.js +45 -45
- package/src/tools/micro/read-file.js +11 -11
- package/src/tools/micro/session-search.js +54 -54
- package/src/tools/micro/shell-exec.js +43 -43
- package/src/tools/micro/trigger-job.js +79 -79
- package/src/tools/micro/web-search.js +58 -58
- package/src/tools/micro/workspace-paths.js +39 -39
- package/src/tools/micro/write-file.js +14 -14
- package/src/tools/micro/write-memory.js +41 -41
- package/src/tools/registry.js +348 -348
- package/src/tools/tool-result-contract.js +36 -36
- package/src/tui/chat.js +835 -835
- package/src/tui/renderer.js +175 -175
- package/src/tui/socket-client.js +217 -217
- package/src/utils/canonical-json.js +29 -29
- package/src/utils/compaction.js +30 -30
- package/src/utils/env-loader.js +5 -5
- package/src/utils/errors.js +80 -80
- package/src/utils/fs.js +101 -101
- package/src/utils/ids.js +5 -5
- package/src/utils/model-context-limits.js +30 -30
- package/src/utils/token-budget.js +74 -74
- package/src/utils/usage-cost.js +25 -25
- package/src/utils/usage-metrics.js +14 -14
package/src/providers/base.js
CHANGED
|
@@ -1,247 +1,247 @@
|
|
|
1
|
-
import { annotateError } from "../runtime/network.js";
|
|
2
|
-
import { fetchWithOutboundPolicy } from "../runtime/ssrf.js";
|
|
3
|
-
import { OUTBOUND_ADDRESS_POLICY } from "../security/ssrf-check.js";
|
|
4
|
-
import { getAuthProfile, resolveProfileSecret } from "../auth/auth-profiles.js";
|
|
5
|
-
|
|
6
|
-
export class ProviderAdapter {
|
|
7
|
-
constructor(config = {}, dependencies = {}) {
|
|
8
|
-
this.config = config;
|
|
9
|
-
this.fetchImpl = dependencies.fetchImpl || globalThis.fetch;
|
|
10
|
-
this.lookupImpl = dependencies.lookupImpl;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
requireFetch() {
|
|
14
|
-
if (!this.fetchImpl) {
|
|
15
|
-
throw new Error("No fetch implementation available");
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
get authRef() {
|
|
20
|
-
return this.config.authRef || "";
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
get authProfileId() {
|
|
24
|
-
if (!this.authRef.startsWith("profile:")) return null;
|
|
25
|
-
return this.authRef.slice("profile:".length);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
resolveAuthProfile() {
|
|
29
|
-
const profileId = this.authProfileId;
|
|
30
|
-
if (!profileId) return null;
|
|
31
|
-
return getAuthProfile(profileId);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
resolveAuthToken() {
|
|
35
|
-
if (this.authRef.startsWith("env:")) {
|
|
36
|
-
const envName = this.authRef.slice(4);
|
|
37
|
-
return process.env[envName] || null;
|
|
38
|
-
}
|
|
39
|
-
if (this.authRef.startsWith("profile:")) {
|
|
40
|
-
return resolveProfileSecret(this.resolveAuthProfile());
|
|
41
|
-
}
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
ensureAuthToken() {
|
|
46
|
-
const token = this.resolveAuthToken();
|
|
47
|
-
if (!token) {
|
|
48
|
-
throw new Error(`Missing auth token for ${this.config.id || "provider"} (${this.authRef})`);
|
|
49
|
-
}
|
|
50
|
-
return token;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
buildUrl(endpoint) {
|
|
54
|
-
const baseUrl = String(this.config.baseUrl || "").replace(/\/$/, "");
|
|
55
|
-
const pathPart = String(endpoint || "").replace(/^\//, "");
|
|
56
|
-
return `${baseUrl}/${pathPart}`;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
buildRequestOptions(options = {}, runtimeOptions = {}) {
|
|
60
|
-
const timeoutMs = Number((runtimeOptions.timeoutMs ?? this.config.defaultTimeoutMs) || 0);
|
|
61
|
-
if (!timeoutMs || typeof AbortSignal?.timeout !== "function") {
|
|
62
|
-
return options;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
...options,
|
|
67
|
-
signal: options.signal || AbortSignal.timeout(timeoutMs)
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
resolveRequestUrl(target) {
|
|
72
|
-
if (typeof target === "string" && /^[a-z]+:\/\//i.test(target)) {
|
|
73
|
-
return target;
|
|
74
|
-
}
|
|
75
|
-
return this.buildUrl(target);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async fetchResponse(target, options = {}, runtimeOptions = {}) {
|
|
79
|
-
this.requireFetch();
|
|
80
|
-
const timeoutMs = Number((runtimeOptions.timeoutMs ?? this.config.defaultTimeoutMs) || 0);
|
|
81
|
-
const isOllama = this.config.id === "ollama" || this.config.adapter === "ollama";
|
|
82
|
-
try {
|
|
83
|
-
// Provider URLs come from runtime configuration. Only Ollama is meant to
|
|
84
|
-
// stay local, and even then it must resolve to loopback.
|
|
85
|
-
return await fetchWithOutboundPolicy(
|
|
86
|
-
this.resolveRequestUrl(target),
|
|
87
|
-
this.buildRequestOptions(options, runtimeOptions),
|
|
88
|
-
{
|
|
89
|
-
fetchImpl: this.fetchImpl,
|
|
90
|
-
lookupImpl: this.lookupImpl,
|
|
91
|
-
surface: "provider",
|
|
92
|
-
privateAddressMessage: `Provider request blocked — target resolves to a private/reserved IP address for ${this.config.id || "provider"}.`,
|
|
93
|
-
loopbackOnlyMessage: `Ollama base URL must resolve to loopback only; refusing ${this.resolveRequestUrl(target)}.`,
|
|
94
|
-
addressPolicy: isOllama ? OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK : OUTBOUND_ADDRESS_POLICY.BLOCK_PRIVATE,
|
|
95
|
-
}
|
|
96
|
-
);
|
|
97
|
-
} catch (error) {
|
|
98
|
-
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
99
|
-
throw annotateError(
|
|
100
|
-
new Error(`Provider request timed out after ${timeoutMs || "configured"} ms for ${this.config.id || "provider"}`),
|
|
101
|
-
{
|
|
102
|
-
surface: "provider"
|
|
103
|
-
}
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
throw annotateError(error, { surface: "provider" });
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
getCapabilities() {
|
|
111
|
-
return {
|
|
112
|
-
supportsToolDefinitions: false,
|
|
113
|
-
supportsToolCalls: false,
|
|
114
|
-
structuredOutputMode: "none",
|
|
115
|
-
supportsReasoningSchema: false,
|
|
116
|
-
tokenCountingMode: "heuristic",
|
|
117
|
-
toolReliabilityTier: "unsupported",
|
|
118
|
-
supportsVision: false
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
supportsNativeStructuredOutput() {
|
|
123
|
-
return new Set(["response_format", "forced_tool", "constrained_decoding"]).has(
|
|
124
|
-
this.getCapabilities().structuredOutputMode
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
buildStructuredOutputFormat(_input) {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async countTokens(_input) {
|
|
133
|
-
return null;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async listModels() {
|
|
137
|
-
return null;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
normalizeResponse(raw) {
|
|
141
|
-
if (!raw) {
|
|
142
|
-
return {
|
|
143
|
-
summary: "Provider returned no data.",
|
|
144
|
-
output: "",
|
|
145
|
-
nextActions: [],
|
|
146
|
-
reasoning: null,
|
|
147
|
-
raw
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const text = this.extractText(raw);
|
|
152
|
-
const parsedJson = text ? this.parseStructuredJson(text) : null;
|
|
153
|
-
if (parsedJson) {
|
|
154
|
-
return {
|
|
155
|
-
summary: parsedJson.summary || "Provider returned structured JSON output.",
|
|
156
|
-
output: parsedJson.output || text,
|
|
157
|
-
nextActions: Array.isArray(parsedJson.nextActions) ? parsedJson.nextActions : [],
|
|
158
|
-
reasoning: parsedJson.analysis || null,
|
|
159
|
-
raw
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (text) {
|
|
164
|
-
return {
|
|
165
|
-
summary: text.slice(0, 200) || "Provider responded.",
|
|
166
|
-
output: text,
|
|
167
|
-
nextActions: [],
|
|
168
|
-
reasoning: null,
|
|
169
|
-
raw
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return {
|
|
174
|
-
summary: "Provider responded with structured data.",
|
|
175
|
-
output: JSON.stringify(raw, null, 2),
|
|
176
|
-
nextActions: [],
|
|
177
|
-
reasoning: null,
|
|
178
|
-
raw
|
|
179
|
-
};
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
extractText(raw) {
|
|
183
|
-
return raw?.content && Array.isArray(raw.content)
|
|
184
|
-
? raw.content
|
|
185
|
-
.map((item) => item.text || item.content || "")
|
|
186
|
-
.filter(Boolean)
|
|
187
|
-
.join("\n")
|
|
188
|
-
: raw?.choices?.[0]?.message?.content || raw?.message?.content
|
|
189
|
-
? String(raw?.choices?.[0]?.message?.content || raw?.message?.content)
|
|
190
|
-
: null;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
parseStructuredJson(text) {
|
|
194
|
-
const trimmed = String(text).trim();
|
|
195
|
-
const fencedMatch = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
196
|
-
const candidate = fencedMatch ? fencedMatch[1].trim() : trimmed;
|
|
197
|
-
if (!(candidate.startsWith("{") && candidate.endsWith("}"))) {
|
|
198
|
-
return null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
try {
|
|
202
|
-
return JSON.parse(candidate);
|
|
203
|
-
} catch {
|
|
204
|
-
return null;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async postJson(endpoint, body, headers = {}, runtimeOptions = {}) {
|
|
209
|
-
let response;
|
|
210
|
-
try {
|
|
211
|
-
response = await this.fetchResponse(
|
|
212
|
-
endpoint,
|
|
213
|
-
{
|
|
214
|
-
method: "POST",
|
|
215
|
-
headers: {
|
|
216
|
-
"content-type": "application/json",
|
|
217
|
-
...headers
|
|
218
|
-
},
|
|
219
|
-
body: JSON.stringify(body)
|
|
220
|
-
},
|
|
221
|
-
runtimeOptions
|
|
222
|
-
);
|
|
223
|
-
} catch (error) {
|
|
224
|
-
throw error;
|
|
225
|
-
}
|
|
226
|
-
const text = await response.text();
|
|
227
|
-
let data = null;
|
|
228
|
-
if (text) {
|
|
229
|
-
try {
|
|
230
|
-
data = JSON.parse(text);
|
|
231
|
-
} catch {
|
|
232
|
-
data = text;
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (!response.ok) {
|
|
237
|
-
throw annotateError(
|
|
238
|
-
new Error(`Provider error ${response.status}: ${typeof data === "string" ? data : JSON.stringify(data)}`),
|
|
239
|
-
{
|
|
240
|
-
surface: "provider"
|
|
241
|
-
}
|
|
242
|
-
);
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
return data;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
1
|
+
import { annotateError } from "../runtime/network.js";
|
|
2
|
+
import { fetchWithOutboundPolicy } from "../runtime/ssrf.js";
|
|
3
|
+
import { OUTBOUND_ADDRESS_POLICY } from "../security/ssrf-check.js";
|
|
4
|
+
import { getAuthProfile, resolveProfileSecret } from "../auth/auth-profiles.js";
|
|
5
|
+
|
|
6
|
+
export class ProviderAdapter {
|
|
7
|
+
constructor(config = {}, dependencies = {}) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.fetchImpl = dependencies.fetchImpl || globalThis.fetch;
|
|
10
|
+
this.lookupImpl = dependencies.lookupImpl;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
requireFetch() {
|
|
14
|
+
if (!this.fetchImpl) {
|
|
15
|
+
throw new Error("No fetch implementation available");
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get authRef() {
|
|
20
|
+
return this.config.authRef || "";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get authProfileId() {
|
|
24
|
+
if (!this.authRef.startsWith("profile:")) return null;
|
|
25
|
+
return this.authRef.slice("profile:".length);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
resolveAuthProfile() {
|
|
29
|
+
const profileId = this.authProfileId;
|
|
30
|
+
if (!profileId) return null;
|
|
31
|
+
return getAuthProfile(profileId);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
resolveAuthToken() {
|
|
35
|
+
if (this.authRef.startsWith("env:")) {
|
|
36
|
+
const envName = this.authRef.slice(4);
|
|
37
|
+
return process.env[envName] || null;
|
|
38
|
+
}
|
|
39
|
+
if (this.authRef.startsWith("profile:")) {
|
|
40
|
+
return resolveProfileSecret(this.resolveAuthProfile());
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ensureAuthToken() {
|
|
46
|
+
const token = this.resolveAuthToken();
|
|
47
|
+
if (!token) {
|
|
48
|
+
throw new Error(`Missing auth token for ${this.config.id || "provider"} (${this.authRef})`);
|
|
49
|
+
}
|
|
50
|
+
return token;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
buildUrl(endpoint) {
|
|
54
|
+
const baseUrl = String(this.config.baseUrl || "").replace(/\/$/, "");
|
|
55
|
+
const pathPart = String(endpoint || "").replace(/^\//, "");
|
|
56
|
+
return `${baseUrl}/${pathPart}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
buildRequestOptions(options = {}, runtimeOptions = {}) {
|
|
60
|
+
const timeoutMs = Number((runtimeOptions.timeoutMs ?? this.config.defaultTimeoutMs) || 0);
|
|
61
|
+
if (!timeoutMs || typeof AbortSignal?.timeout !== "function") {
|
|
62
|
+
return options;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
...options,
|
|
67
|
+
signal: options.signal || AbortSignal.timeout(timeoutMs)
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
resolveRequestUrl(target) {
|
|
72
|
+
if (typeof target === "string" && /^[a-z]+:\/\//i.test(target)) {
|
|
73
|
+
return target;
|
|
74
|
+
}
|
|
75
|
+
return this.buildUrl(target);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async fetchResponse(target, options = {}, runtimeOptions = {}) {
|
|
79
|
+
this.requireFetch();
|
|
80
|
+
const timeoutMs = Number((runtimeOptions.timeoutMs ?? this.config.defaultTimeoutMs) || 0);
|
|
81
|
+
const isOllama = this.config.id === "ollama" || this.config.adapter === "ollama";
|
|
82
|
+
try {
|
|
83
|
+
// Provider URLs come from runtime configuration. Only Ollama is meant to
|
|
84
|
+
// stay local, and even then it must resolve to loopback.
|
|
85
|
+
return await fetchWithOutboundPolicy(
|
|
86
|
+
this.resolveRequestUrl(target),
|
|
87
|
+
this.buildRequestOptions(options, runtimeOptions),
|
|
88
|
+
{
|
|
89
|
+
fetchImpl: this.fetchImpl,
|
|
90
|
+
lookupImpl: this.lookupImpl,
|
|
91
|
+
surface: "provider",
|
|
92
|
+
privateAddressMessage: `Provider request blocked — target resolves to a private/reserved IP address for ${this.config.id || "provider"}.`,
|
|
93
|
+
loopbackOnlyMessage: `Ollama base URL must resolve to loopback only; refusing ${this.resolveRequestUrl(target)}.`,
|
|
94
|
+
addressPolicy: isOllama ? OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK : OUTBOUND_ADDRESS_POLICY.BLOCK_PRIVATE,
|
|
95
|
+
}
|
|
96
|
+
);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error?.name === "TimeoutError" || error?.name === "AbortError") {
|
|
99
|
+
throw annotateError(
|
|
100
|
+
new Error(`Provider request timed out after ${timeoutMs || "configured"} ms for ${this.config.id || "provider"}`),
|
|
101
|
+
{
|
|
102
|
+
surface: "provider"
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
throw annotateError(error, { surface: "provider" });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
getCapabilities() {
|
|
111
|
+
return {
|
|
112
|
+
supportsToolDefinitions: false,
|
|
113
|
+
supportsToolCalls: false,
|
|
114
|
+
structuredOutputMode: "none",
|
|
115
|
+
supportsReasoningSchema: false,
|
|
116
|
+
tokenCountingMode: "heuristic",
|
|
117
|
+
toolReliabilityTier: "unsupported",
|
|
118
|
+
supportsVision: false
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
supportsNativeStructuredOutput() {
|
|
123
|
+
return new Set(["response_format", "forced_tool", "constrained_decoding"]).has(
|
|
124
|
+
this.getCapabilities().structuredOutputMode
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
buildStructuredOutputFormat(_input) {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async countTokens(_input) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async listModels() {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
normalizeResponse(raw) {
|
|
141
|
+
if (!raw) {
|
|
142
|
+
return {
|
|
143
|
+
summary: "Provider returned no data.",
|
|
144
|
+
output: "",
|
|
145
|
+
nextActions: [],
|
|
146
|
+
reasoning: null,
|
|
147
|
+
raw
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const text = this.extractText(raw);
|
|
152
|
+
const parsedJson = text ? this.parseStructuredJson(text) : null;
|
|
153
|
+
if (parsedJson) {
|
|
154
|
+
return {
|
|
155
|
+
summary: parsedJson.summary || "Provider returned structured JSON output.",
|
|
156
|
+
output: parsedJson.output || text,
|
|
157
|
+
nextActions: Array.isArray(parsedJson.nextActions) ? parsedJson.nextActions : [],
|
|
158
|
+
reasoning: parsedJson.analysis || null,
|
|
159
|
+
raw
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (text) {
|
|
164
|
+
return {
|
|
165
|
+
summary: text.slice(0, 200) || "Provider responded.",
|
|
166
|
+
output: text,
|
|
167
|
+
nextActions: [],
|
|
168
|
+
reasoning: null,
|
|
169
|
+
raw
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
summary: "Provider responded with structured data.",
|
|
175
|
+
output: JSON.stringify(raw, null, 2),
|
|
176
|
+
nextActions: [],
|
|
177
|
+
reasoning: null,
|
|
178
|
+
raw
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
extractText(raw) {
|
|
183
|
+
return raw?.content && Array.isArray(raw.content)
|
|
184
|
+
? raw.content
|
|
185
|
+
.map((item) => item.text || item.content || "")
|
|
186
|
+
.filter(Boolean)
|
|
187
|
+
.join("\n")
|
|
188
|
+
: raw?.choices?.[0]?.message?.content || raw?.message?.content
|
|
189
|
+
? String(raw?.choices?.[0]?.message?.content || raw?.message?.content)
|
|
190
|
+
: null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
parseStructuredJson(text) {
|
|
194
|
+
const trimmed = String(text).trim();
|
|
195
|
+
const fencedMatch = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
|
|
196
|
+
const candidate = fencedMatch ? fencedMatch[1].trim() : trimmed;
|
|
197
|
+
if (!(candidate.startsWith("{") && candidate.endsWith("}"))) {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
try {
|
|
202
|
+
return JSON.parse(candidate);
|
|
203
|
+
} catch {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async postJson(endpoint, body, headers = {}, runtimeOptions = {}) {
|
|
209
|
+
let response;
|
|
210
|
+
try {
|
|
211
|
+
response = await this.fetchResponse(
|
|
212
|
+
endpoint,
|
|
213
|
+
{
|
|
214
|
+
method: "POST",
|
|
215
|
+
headers: {
|
|
216
|
+
"content-type": "application/json",
|
|
217
|
+
...headers
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify(body)
|
|
220
|
+
},
|
|
221
|
+
runtimeOptions
|
|
222
|
+
);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
throw error;
|
|
225
|
+
}
|
|
226
|
+
const text = await response.text();
|
|
227
|
+
let data = null;
|
|
228
|
+
if (text) {
|
|
229
|
+
try {
|
|
230
|
+
data = JSON.parse(text);
|
|
231
|
+
} catch {
|
|
232
|
+
data = text;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!response.ok) {
|
|
237
|
+
throw annotateError(
|
|
238
|
+
new Error(`Provider error ${response.status}: ${typeof data === "string" ? data : JSON.stringify(data)}`),
|
|
239
|
+
{
|
|
240
|
+
surface: "provider"
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return data;
|
|
246
|
+
}
|
|
247
|
+
}
|