loopengine 0.1.73 → 0.1.74
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/dist/core/secret-store.js +9 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/web/global-config-page.js +64 -3
- package/dist/web/global-config.d.ts +14 -0
- package/dist/web/global-config.js +32 -0
- package/package.json +1 -1
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// self-hosted install has always used. A host that keeps secrets
|
|
6
6
|
// somewhere else (a hosted control plane's vault, Vault, a cloud secret
|
|
7
7
|
// manager) swaps it in once at startup with setSecretStore().
|
|
8
|
-
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
8
|
+
import { chmodSync, existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
9
|
import { join } from 'node:path';
|
|
10
10
|
import { parseEnv } from 'node:util';
|
|
11
11
|
export function agentEnvFilePath(dir) {
|
|
@@ -72,7 +72,13 @@ function upsertEnvFileLine(path, name, value) {
|
|
|
72
72
|
else {
|
|
73
73
|
lines[existingIndex] = newLine;
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
writeEnvFile(path, lines.join('\n') + '\n');
|
|
76
|
+
}
|
|
77
|
+
/** .env files hold secrets: owner-only (0600), including one that
|
|
78
|
+
* already existed with wider permissions. */
|
|
79
|
+
function writeEnvFile(path, content) {
|
|
80
|
+
writeFileSync(path, content, { mode: 0o600 });
|
|
81
|
+
chmodSync(path, 0o600);
|
|
76
82
|
}
|
|
77
83
|
/** Removes NAME= from the .env file at `path`, if present. Only
|
|
78
84
|
* rewrites the file when something actually changed — a no-op removal
|
|
@@ -84,7 +90,7 @@ function removeEnvFileLine(path, name) {
|
|
|
84
90
|
const lines = readFileSync(path, 'utf8').split('\n');
|
|
85
91
|
const filtered = lines.filter((line) => !line.startsWith(`${name}=`));
|
|
86
92
|
if (filtered.length !== lines.length)
|
|
87
|
-
|
|
93
|
+
writeEnvFile(path, filtered.join('\n').replace(/\n*$/, '\n'));
|
|
88
94
|
}
|
|
89
95
|
/** The project's .env for shared values, agents/<name>/.env for each
|
|
90
96
|
* agent's own — the default, and what every self-hosted install uses. */
|
package/dist/index.d.ts
CHANGED
|
@@ -77,7 +77,7 @@ export { installAbility, upgradeAbility, removeAbility, listInstalledAbilities,
|
|
|
77
77
|
export type { InstalledAbilityRecord, AbilityEnvDecl, UpgradeFileResult } from './bin/ability-manager.js';
|
|
78
78
|
export { searchPublicAbilities, fetchLatestAbilityVersion } from './web/abilities-admin.js';
|
|
79
79
|
export type { PublicAbilitySearchResult } from './web/abilities-admin.js';
|
|
80
|
-
export { describeModelProviders, describeGateways } from './web/global-config.js';
|
|
80
|
+
export { describeModelProviders, describeGateways, setModelProviderKey, clearModelProviderKey, ModelKeyError } from './web/global-config.js';
|
|
81
81
|
export type { ModelProviderInfo, AgentModelUsage, ModelsView, GatewayProviderInfo, GatewaysView } from './web/global-config.js';
|
|
82
82
|
export { createTrackedApprover, listApprovals, decideApproval, findApproval } from './web/web-approver.js';
|
|
83
83
|
export { scaffoldAgent, scaffoldSubagent, AgentNameError, AgentExistsError, AgentNotFoundError, AgentModelError } from './bin/cli.js';
|
package/dist/index.js
CHANGED
|
@@ -68,6 +68,6 @@ export { listDeclaredEnvVars, setEnvVar, unsetEnvVar, setAgentEnvVar, unsetAgent
|
|
|
68
68
|
export { readActauthConfig, addActauthRule, updateActauthRule, removeActauthRule, setDefaultDecision, ActauthRuleExistsError, ActauthRuleNotFoundError, } from './web/actauth-admin.js';
|
|
69
69
|
export { installAbility, upgradeAbility, removeAbility, listInstalledAbilities, backfillDependencies, AbilityManifestError, AbilityVersionError, AbilityCollisionError, AbilityAlreadyInstalledError, AbilityNotInstalledError, } from './bin/ability-manager.js';
|
|
70
70
|
export { searchPublicAbilities, fetchLatestAbilityVersion } from './web/abilities-admin.js';
|
|
71
|
-
export { describeModelProviders, describeGateways } from './web/global-config.js';
|
|
71
|
+
export { describeModelProviders, describeGateways, setModelProviderKey, clearModelProviderKey, ModelKeyError } from './web/global-config.js';
|
|
72
72
|
export { createTrackedApprover, listApprovals, decideApproval, findApproval } from './web/web-approver.js';
|
|
73
73
|
export { scaffoldAgent, scaffoldSubagent, AgentNameError, AgentExistsError, AgentNotFoundError, AgentModelError } from './bin/cli.js';
|
|
@@ -62,6 +62,11 @@ export const globalConfigPageHtml = `<!doctype html>
|
|
|
62
62
|
border-color: var(--bad);
|
|
63
63
|
}
|
|
64
64
|
.delete-btn:hover { background: var(--bad-soft); }
|
|
65
|
+
form.key-form { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; margin: 0; }
|
|
66
|
+
form.key-form input[type=password] { width: 240px; max-width: 100%; font-family: ui-monospace, monospace; font-size: 12px; }
|
|
67
|
+
form.key-form button { font-size: 12px; }
|
|
68
|
+
.key-msg { font-size: 11px; margin-top: 4px; }
|
|
69
|
+
.key-msg.bad { color: var(--bad); }
|
|
65
70
|
</style>
|
|
66
71
|
</head>
|
|
67
72
|
<body data-le-page="config">
|
|
@@ -70,7 +75,7 @@ ${consoleSidebarHtml}
|
|
|
70
75
|
<main class="le-main">
|
|
71
76
|
<div class="section-panel" data-section-panel="models" style="display:contents">
|
|
72
77
|
<div class="le-head plain"><div class="le-title"><button type="button" class="le-menu-btn" data-le-menu aria-label="Open navigation">☰</button>
|
|
73
|
-
<div><h1>Models</h1><p>
|
|
78
|
+
<div><h1>Models</h1><p>API keys for each model provider, and which agents use each one. A saved key is stored on this server and never shown again.</p></div></div></div>
|
|
74
79
|
<div class="le-scroll"><div class="le-page" style="display:block"><div id="modelsContent"><p class="hint">Loading…</p></div></div></div>
|
|
75
80
|
</div>
|
|
76
81
|
<div class="section-panel" data-section-panel="gateways" style="display:none">
|
|
@@ -102,9 +107,19 @@ ${consoleShellScript}
|
|
|
102
107
|
|
|
103
108
|
function renderModels(data) {
|
|
104
109
|
var providerRows = data.providers.map(function (p) {
|
|
110
|
+
// Write-only: the field is always empty; the server never sends a
|
|
111
|
+
// key back, only whether one is set.
|
|
112
|
+
var keyCell = data.canSave
|
|
113
|
+
? '<form class="key-form" data-provider="' + escapeHtml(p.provider) + '">' +
|
|
114
|
+
'<input type="password" name="key" autocomplete="off" spellcheck="false" placeholder="' + (p.configured ? 'Paste a new key to replace it' : 'Paste API key') + '" aria-label="' + escapeHtml(p.provider) + ' API key">' +
|
|
115
|
+
'<button type="submit">Save</button>' +
|
|
116
|
+
(p.configured ? '<button type="button" class="delete-btn" data-remove-key="' + escapeHtml(p.provider) + '">Remove</button>' : '') +
|
|
117
|
+
'</form><div class="key-msg" data-key-msg="' + escapeHtml(p.provider) + '"></div>'
|
|
118
|
+
: '<span class="hint">Set LOOPENGINE_ADMIN_AUTH to manage keys here, or put ' + escapeHtml(p.envVar) + ' in .env.</span>';
|
|
105
119
|
return '<tr><td><code>' + escapeHtml(p.provider) + '</code></td>' +
|
|
106
120
|
'<td><code>' + escapeHtml(p.envVar) + '</code></td>' +
|
|
107
|
-
'<td>' + badge(p.configured, 'configured', 'not set') + '</td
|
|
121
|
+
'<td>' + badge(p.configured, 'configured', 'not set') + '</td>' +
|
|
122
|
+
'<td>' + keyCell + '</td></tr>';
|
|
108
123
|
}).join('');
|
|
109
124
|
|
|
110
125
|
var agentRows = data.agents.length
|
|
@@ -116,13 +131,59 @@ ${consoleShellScript}
|
|
|
116
131
|
: '<tr><td colspan="3"><p class="hint">No agents with a resolved model config (a custom createModelCall has nothing to report here).</p></td></tr>';
|
|
117
132
|
|
|
118
133
|
return '<section><h3>Providers</h3>' +
|
|
119
|
-
'<table><thead><tr><th>Provider</th><th>Env var</th><th>Status</th></tr></thead><tbody>' + providerRows + '</tbody></table>' +
|
|
134
|
+
'<table><thead><tr><th>Provider</th><th>Env var</th><th>Status</th><th>Key</th></tr></thead><tbody>' + providerRows + '</tbody></table>' +
|
|
120
135
|
'</section>' +
|
|
121
136
|
'<section><h3>Agents</h3>' +
|
|
122
137
|
'<table><thead><tr><th>Agent</th><th>Provider</th><th>Model</th></tr></thead><tbody>' + agentRows + '</tbody></table>' +
|
|
123
138
|
'</section>';
|
|
124
139
|
}
|
|
125
140
|
|
|
141
|
+
function keyMessage(provider, text, bad) {
|
|
142
|
+
var el = document.querySelector('[data-key-msg="' + provider + '"]');
|
|
143
|
+
if (!el) return;
|
|
144
|
+
el.textContent = text;
|
|
145
|
+
el.className = 'key-msg' + (bad ? ' bad' : '');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function sendKey(provider, method, body) {
|
|
149
|
+
keyMessage(provider, method === 'PUT' ? 'Saving' + String.fromCharCode(8230) : 'Removing' + String.fromCharCode(8230), false);
|
|
150
|
+
return fetch('/config/models/' + encodeURIComponent(provider) + '/key', {
|
|
151
|
+
method: method,
|
|
152
|
+
headers: body ? { 'content-type': 'application/json' } : {},
|
|
153
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
154
|
+
})
|
|
155
|
+
.then(function (r) { return r.json().then(function (j) { return { ok: r.ok, body: j }; }); })
|
|
156
|
+
.then(function (r) {
|
|
157
|
+
if (!r.ok) throw new Error(r.body.error || 'request failed');
|
|
158
|
+
loadModels();
|
|
159
|
+
})
|
|
160
|
+
.catch(function (err) { keyMessage(provider, err.message, true); });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
document.getElementById('modelsContent').addEventListener('submit', function (ev) {
|
|
164
|
+
var form = ev.target.closest('form.key-form');
|
|
165
|
+
if (!form) return;
|
|
166
|
+
ev.preventDefault();
|
|
167
|
+
var input = form.querySelector('input[name=key]');
|
|
168
|
+
var key = input.value.trim();
|
|
169
|
+
if (!key) { keyMessage(form.getAttribute('data-provider'), 'Paste a key first.', true); return; }
|
|
170
|
+
// Cleared at once, so the key doesn't sit in the page while saving.
|
|
171
|
+
input.value = '';
|
|
172
|
+
sendKey(form.getAttribute('data-provider'), 'PUT', { key: key });
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
document.getElementById('modelsContent').addEventListener('click', function (ev) {
|
|
176
|
+
var btn = ev.target.closest('[data-remove-key]');
|
|
177
|
+
if (!btn) return;
|
|
178
|
+
var provider = btn.getAttribute('data-remove-key');
|
|
179
|
+
if (btn.getAttribute('data-armed') !== '1') {
|
|
180
|
+
btn.setAttribute('data-armed', '1');
|
|
181
|
+
btn.textContent = 'Remove ' + provider + ' key?';
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
sendKey(provider, 'DELETE');
|
|
185
|
+
});
|
|
186
|
+
|
|
126
187
|
function loadModels() {
|
|
127
188
|
var content = document.getElementById('modelsContent');
|
|
128
189
|
fetch('/config/models')
|
|
@@ -13,6 +13,8 @@ export interface ModelsView {
|
|
|
13
13
|
providers: ModelProviderInfo[];
|
|
14
14
|
agents: AgentModelUsage[];
|
|
15
15
|
}
|
|
16
|
+
export declare class ModelKeyError extends Error {
|
|
17
|
+
}
|
|
16
18
|
/** Every registered agent's resolved model, alongside whether each
|
|
17
19
|
* supported provider actually has credentials available in this
|
|
18
20
|
* process's environment — an operator's first question when a request
|
|
@@ -55,3 +57,15 @@ export interface GatewaysView {
|
|
|
55
57
|
* shown so the Gateways panel isn't silently missing them, not because
|
|
56
58
|
* there's anything to check for them yet. */
|
|
57
59
|
export declare function describeGateways(cliCommand?: string): Promise<GatewaysView>;
|
|
60
|
+
/** The env var a provider's key lives in, or undefined for an unknown
|
|
61
|
+
* provider. */
|
|
62
|
+
export declare function modelKeyEnvVar(provider: string): string | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Saves a provider's API key from the console's Models page, through the
|
|
65
|
+
* same secret store as the Environment tab (the project's .env by
|
|
66
|
+
* default), and applies it to this process, so the next model call uses
|
|
67
|
+
* it without a restart. Write-only: nothing here or in describeModelProviders
|
|
68
|
+
* ever returns a key, only whether one is set.
|
|
69
|
+
*/
|
|
70
|
+
export declare function setModelProviderKey(provider: string, key: unknown): Promise<void>;
|
|
71
|
+
export declare function clearModelProviderKey(provider: string): Promise<void>;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { getComposioAuthStatus } from '../core/gateway-tools.js';
|
|
2
|
+
import { setEnvVar, unsetEnvVar } from './env-admin.js';
|
|
3
|
+
export class ModelKeyError extends Error {
|
|
4
|
+
}
|
|
2
5
|
// One entry per provider agent-config.ts's own AgentModelConfig union
|
|
3
6
|
// actually supports (see its own doc comment) — kept here, not derived
|
|
4
7
|
// from that type, since there's no runtime list to introspect a TS
|
|
@@ -70,3 +73,32 @@ export async function describeGateways(cliCommand = 'composio') {
|
|
|
70
73
|
],
|
|
71
74
|
};
|
|
72
75
|
}
|
|
76
|
+
/** The env var a provider's key lives in, or undefined for an unknown
|
|
77
|
+
* provider. */
|
|
78
|
+
export function modelKeyEnvVar(provider) {
|
|
79
|
+
return KNOWN_PROVIDERS.find((p) => p.provider === provider)?.envVar;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Saves a provider's API key from the console's Models page, through the
|
|
83
|
+
* same secret store as the Environment tab (the project's .env by
|
|
84
|
+
* default), and applies it to this process, so the next model call uses
|
|
85
|
+
* it without a restart. Write-only: nothing here or in describeModelProviders
|
|
86
|
+
* ever returns a key, only whether one is set.
|
|
87
|
+
*/
|
|
88
|
+
export async function setModelProviderKey(provider, key) {
|
|
89
|
+
const envVar = modelKeyEnvVar(provider);
|
|
90
|
+
if (!envVar)
|
|
91
|
+
throw new ModelKeyError(`unknown provider '${provider}'`);
|
|
92
|
+
const value = typeof key === 'string' ? key.trim() : '';
|
|
93
|
+
// Keys are single tokens; whitespace inside one is a paste mistake.
|
|
94
|
+
if (value.length < 8 || value.length > 500 || /\s/.test(value)) {
|
|
95
|
+
throw new ModelKeyError('That doesn\'t look like an API key: paste the whole key, with no spaces or line breaks.');
|
|
96
|
+
}
|
|
97
|
+
await setEnvVar(envVar, value);
|
|
98
|
+
}
|
|
99
|
+
export async function clearModelProviderKey(provider) {
|
|
100
|
+
const envVar = modelKeyEnvVar(provider);
|
|
101
|
+
if (!envVar)
|
|
102
|
+
throw new ModelKeyError(`unknown provider '${provider}'`);
|
|
103
|
+
await unsetEnvVar(envVar);
|
|
104
|
+
}
|