@elliotsystems/eai 0.1.3 → 0.1.5
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/README.md +93 -54
- package/bin/eai +154 -0
- package/lib/connect.js +205 -0
- package/lib/gdrive.js +238 -0
- package/lib/logout.js +73 -0
- package/lib/resolve.js +9 -15
- package/lib/setup.js +600 -0
- package/lib/stage.js +45 -4
- package/lib/status.js +133 -0
- package/lib/token-env.js +53 -17
- package/lib/update-check.js +27 -0
- package/lib/update.js +303 -0
- package/package.json +2 -4
- package/plugin/opencode-plugin/src/classifier.ts +2 -1
- package/plugin/opencode-plugin/src/elliot-auth.ts +56 -104
- package/plugin/opencode-plugin/src/identity.ts +69 -0
- package/plugin/opencode-plugin/src/index.ts +13 -28
- package/plugin/opencode-plugin/src/mcp-keys.ts +128 -1
- package/plugin/opencode-plugin/src/policy.ts +36 -4
- package/plugin/opencode-plugin/src/proxy-provider.ts +124 -72
- package/plugin/opencode-plugin/src/scope.ts +2 -1
- package/plugin/opencode-plugin/src/tui/knowledge.ts +9 -3
|
@@ -1,96 +1,148 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The Elliot
|
|
3
|
-
*
|
|
4
|
-
* `auth` hook whose `loader` returns a self-refreshing `fetch` that attaches
|
|
5
|
-
* a live bearer token per request.
|
|
2
|
+
* The Elliot provider: a `config` hook that registers a custom `elliot`
|
|
3
|
+
* OpenCode provider pointed at LiteLLM directly.
|
|
6
4
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* - The loader only runs once a stored auth entry exists for its
|
|
16
|
-
* `provider` id (`provider.ts:1584`); the plugin seeds that entry itself
|
|
17
|
-
* at init (see `index.ts`) so no interactive `opencode auth login` step
|
|
18
|
-
* is ever required.
|
|
5
|
+
* Since `/llm-proxy` was removed, this no longer needs a self-refreshing
|
|
6
|
+
* per-request `auth` hook (Task-1's pinned mechanism, `.superpowers/sdd/
|
|
7
|
+
* 2026-09-03-eai-proxy-auth/task-1-findings.md` — kept only as history: the
|
|
8
|
+
* per-instance JWT that mechanism refreshed doesn't authenticate this path
|
|
9
|
+
* anymore). LiteLLM's `litellm_master_key` is ONE static secret for the whole
|
|
10
|
+
* gateway — it never expires, so it is baked into `options.apiKey` directly,
|
|
11
|
+
* the same way OpenCode's own `{env:VAR}` config substitution would, just
|
|
12
|
+
* read from `~/.elliot/config.json` instead of the process environment.
|
|
19
13
|
*
|
|
20
14
|
* Everything here is a no-op when the user isn't logged in
|
|
21
15
|
* (`readElliotConfig()` returns null) — OpenCode then runs exactly as it
|
|
22
16
|
* would without this plugin.
|
|
23
17
|
*/
|
|
24
|
-
import {
|
|
18
|
+
import { readElliotConfig, type ElliotCredential } from "./elliot-auth.js"
|
|
25
19
|
|
|
26
|
-
export const ELLIOT_LLM_PROXY_URL = "https://backend.elliotsystemsonline.com/llm-proxy"
|
|
27
20
|
export const ELLIOT_PROVIDER_ID = "elliot"
|
|
21
|
+
export const ELLIOT_GENERAL_MODEL_ID = "elliot-general"
|
|
22
|
+
export const ELLIOT_CODER_MODEL_ID = "elliot-coder"
|
|
23
|
+
|
|
24
|
+
const ALL_MODELS = {
|
|
25
|
+
[ELLIOT_GENERAL_MODEL_ID]: { name: "Elliot General" },
|
|
26
|
+
[ELLIOT_CODER_MODEL_ID]: { name: "Elliot Coder" },
|
|
27
|
+
}
|
|
28
28
|
|
|
29
29
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
* The `models` block for one registered provider, filtered to
|
|
31
|
+
* `allowedModels` (Phase 4 governance, docs/LITELLM_GATEWAY.md §14) —
|
|
32
|
+
* LiteLLM gateway audit finding #5, mirrors the identical fix already made
|
|
33
|
+
* to `app/services/opencode/teamconfig.py::build_opencode_config` and
|
|
34
|
+
* `elliot-launcher/lib/setup.js::elliotProvider`. LiteLLM was always the
|
|
35
|
+
* real enforcement point regardless of what this offers; this only stops
|
|
36
|
+
* the menu from offering a model that will be rejected. Falsy
|
|
37
|
+
* (undefined/null/empty) — every team until an admin restricts it — offers
|
|
38
|
+
* both, unchanged from before this parameter existed.
|
|
39
|
+
*
|
|
40
|
+
* A team restricted to ONLY elliot-classifier (never offered here —
|
|
41
|
+
* internal-only, deploy/litellm/config.yaml §5) would otherwise get an
|
|
42
|
+
* empty, unusable menu; degrades to offering both for that listing only,
|
|
43
|
+
* same as the two sibling generators — LiteLLM still rejects any actual
|
|
44
|
+
* request the team isn't allowed to make either way.
|
|
34
45
|
*/
|
|
35
|
-
|
|
36
|
-
if (!
|
|
46
|
+
function offeredModels(allowedModels?: string[] | null): typeof ALL_MODELS {
|
|
47
|
+
if (!allowedModels || allowedModels.length === 0) return ALL_MODELS
|
|
37
48
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
models: { "elliot-governed": { name: "Elliot (governed)" } },
|
|
44
|
-
}
|
|
49
|
+
const allowed = new Set(allowedModels)
|
|
50
|
+
const offered = Object.keys(ALL_MODELS).filter((id) => allowed.has(id)) as Array<
|
|
51
|
+
keyof typeof ALL_MODELS
|
|
52
|
+
>
|
|
53
|
+
if (offered.length === 0) return ALL_MODELS
|
|
45
54
|
|
|
46
|
-
|
|
55
|
+
const filtered = {} as typeof ALL_MODELS
|
|
56
|
+
for (const id of offered) filtered[id] = ALL_MODELS[id]
|
|
57
|
+
return filtered
|
|
47
58
|
}
|
|
48
59
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
60
|
+
function registerProvider(
|
|
61
|
+
cfg: any,
|
|
62
|
+
base: string,
|
|
63
|
+
providerId: string,
|
|
64
|
+
label: string,
|
|
65
|
+
apiKey: string,
|
|
66
|
+
allowedModels?: string[] | null,
|
|
67
|
+
): void {
|
|
68
|
+
cfg.provider ??= {}
|
|
69
|
+
// Never clobber a provider id the user (or an earlier plugin) already
|
|
70
|
+
// configured — same "additive only" contract as the single-provider path
|
|
71
|
+
// this replaces.
|
|
72
|
+
cfg.provider[providerId] ??= {
|
|
73
|
+
npm: "@ai-sdk/openai-compatible",
|
|
74
|
+
name: label,
|
|
75
|
+
options: { baseURL: `${base}/v1`, apiKey },
|
|
76
|
+
models: offeredModels(allowedModels),
|
|
77
|
+
}
|
|
58
78
|
}
|
|
59
79
|
|
|
60
80
|
/**
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
81
|
+
* Registers one OpenCode provider per LiteLLM credential this person has
|
|
82
|
+
* (Phase 4, docs/LITELLM_GATEWAY.md §14): their own personal key under
|
|
83
|
+
* `elliot`, plus one `elliot-team-<slug>` provider per team they belong to —
|
|
84
|
+
* so `opencode.json`'s own global+project-local merge, not this plugin, is
|
|
85
|
+
* what decides which key a given folder actually uses (a project-local
|
|
86
|
+
* config pins `model` to a team's provider; the global default falls back to
|
|
87
|
+
* the personal one). Mutating `cfg` in place.
|
|
88
|
+
*
|
|
89
|
+
* Falls back to registering a single `elliot` provider from the legacy
|
|
90
|
+
* `litellm_master_key` field when `elliot.credentials` is absent — an
|
|
91
|
+
* `eai setup` written before this phase, or a backend that predates it,
|
|
92
|
+
* keeps working unchanged rather than losing LLM access outright.
|
|
93
|
+
*
|
|
94
|
+
* No-op when the user isn't logged in. Never overwrites a model the user
|
|
95
|
+
* already configured, or any other existing key under
|
|
96
|
+
* `cfg.provider`/`cfg.plugin`/`cfg.mcp`/etc.
|
|
66
97
|
*/
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
98
|
+
export function configHook(cfg: any): void {
|
|
99
|
+
const elliot = readElliotConfig()
|
|
100
|
+
if (!elliot?.litellm_base_url) return
|
|
101
|
+
|
|
102
|
+
const base = elliot.litellm_base_url.replace(/\/+$/, "")
|
|
103
|
+
|
|
104
|
+
// The DEFAULT provider's own offered models decide the default `model`
|
|
105
|
+
// below — never just `elliot-general` unconditionally, since the
|
|
106
|
+
// fallback (single-provider) path can itself be restricted. In the
|
|
107
|
+
// credentials path this is always the personal credential (provider_id
|
|
108
|
+
// "elliot"), which has no team and so is never restricted — `ALL_MODELS`
|
|
109
|
+
// unless a future personal restriction concept changes that.
|
|
110
|
+
let defaultProviderModels: typeof ALL_MODELS = ALL_MODELS
|
|
111
|
+
|
|
112
|
+
if (elliot.credentials?.length) {
|
|
113
|
+
for (const credential of elliot.credentials as ElliotCredential[]) {
|
|
114
|
+
if (!credential.provider_id || !credential.api_key) continue
|
|
115
|
+
registerProvider(
|
|
116
|
+
cfg,
|
|
117
|
+
base,
|
|
118
|
+
credential.provider_id,
|
|
119
|
+
credential.label,
|
|
120
|
+
credential.api_key,
|
|
121
|
+
credential.allowed_models,
|
|
122
|
+
)
|
|
123
|
+
if (credential.provider_id === ELLIOT_PROVIDER_ID) {
|
|
124
|
+
defaultProviderModels = offeredModels(credential.allowed_models)
|
|
125
|
+
}
|
|
73
126
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
127
|
+
} else if (elliot.litellm_master_key) {
|
|
128
|
+
registerProvider(
|
|
129
|
+
cfg,
|
|
130
|
+
base,
|
|
131
|
+
ELLIOT_PROVIDER_ID,
|
|
132
|
+
"Elliot (LiteLLM)",
|
|
133
|
+
elliot.litellm_master_key,
|
|
134
|
+
elliot.allowed_models,
|
|
135
|
+
)
|
|
136
|
+
defaultProviderModels = offeredModels(elliot.allowed_models)
|
|
137
|
+
} else {
|
|
138
|
+
return
|
|
77
139
|
}
|
|
78
|
-
}
|
|
79
140
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
export function makeAuthHook() {
|
|
88
|
-
return {
|
|
89
|
-
provider: ELLIOT_PROVIDER_ID,
|
|
90
|
-
methods: [] as unknown[],
|
|
91
|
-
loader: async (_auth: () => Promise<unknown>, _provider: unknown) => ({
|
|
92
|
-
apiKey: "elliot-managed",
|
|
93
|
-
fetch: elliotFetch,
|
|
94
|
-
}),
|
|
95
|
-
}
|
|
141
|
+
// Follows the restriction, not always General — a team restricted to
|
|
142
|
+
// Coder-only must not default to a model its own menu just excluded.
|
|
143
|
+
const defaultModelId =
|
|
144
|
+
ELLIOT_GENERAL_MODEL_ID in defaultProviderModels
|
|
145
|
+
? ELLIOT_GENERAL_MODEL_ID
|
|
146
|
+
: Object.keys(defaultProviderModels)[0]
|
|
147
|
+
cfg.model ??= `${ELLIOT_PROVIDER_ID}/${defaultModelId}`
|
|
96
148
|
}
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Repository scope from the session worktree — the trusted-context half of the
|
|
3
3
|
* "the model never names a repository" invariant.
|
|
4
4
|
*
|
|
5
|
-
* Ported (trimmed, not imported) from
|
|
5
|
+
* Ported (trimmed, not imported) from the retired elliot-ai CLI's
|
|
6
|
+
* `src/project.ts`: the same
|
|
6
7
|
* `execFile` discipline — a fixed, hardcoded argument array, never a string a
|
|
7
8
|
* shell would parse, and never anything the LLM supplies. The worktree path
|
|
8
9
|
* comes from OpenCode's own `PluginInput`/`ToolContext`, which is session
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* proven from session state or not shown.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { KNOWLEDGE_TOOL, toCanonicalToolUnderAnyGateway } from "../mcp-keys.js"
|
|
18
18
|
|
|
19
19
|
// ------------------------------------------------------------- host shapes --
|
|
20
20
|
|
|
@@ -106,9 +106,15 @@ export function elliotServerNames(items: readonly McpItem[]): string[] {
|
|
|
106
106
|
* reasoning applies to a panel that would otherwise attribute a third party's
|
|
107
107
|
* results to Elliot.
|
|
108
108
|
*/
|
|
109
|
-
export function isKnowledgeTool(
|
|
109
|
+
export function isKnowledgeTool(
|
|
110
|
+
tool: string | undefined,
|
|
111
|
+
serverNames: readonly string[],
|
|
112
|
+
): boolean {
|
|
110
113
|
if (typeof tool !== "string") return false
|
|
111
|
-
|
|
114
|
+
// Alias-agnostic BY NECESSITY, not by preference — see
|
|
115
|
+
// `toCanonicalToolUnderAnyGateway` for why the TUI cannot know the
|
|
116
|
+
// configured alias, and why that is safe for a display surface.
|
|
117
|
+
return toCanonicalToolUnderAnyGateway(tool, serverNames) === KNOWLEDGE_TOOL
|
|
112
118
|
}
|
|
113
119
|
|
|
114
120
|
/**
|