@qadanm/opencode-smart-router 0.0.0-stage → 0.4.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/CHANGELOG.md +118 -0
- package/CONTRIBUTING.md +96 -0
- package/LICENSE +21 -0
- package/README.md +341 -2
- package/SECURITY.md +174 -0
- package/config.example.json +108 -0
- package/dist/allowance.js +487 -0
- package/dist/gui-experimental.cjs +1 -0
- package/dist/index.js +11111 -0
- package/dist/npm/index.js +40 -0
- package/dist/tui.js +587 -0
- package/docs/ARCHITECTURE.md +104 -0
- package/docs/DEBUGGING-GUI.md +70 -0
- package/docs/INSTALL.md +120 -0
- package/docs/LIMITATIONS.md +213 -0
- package/docs/MODELS.md +124 -0
- package/docs/OPENDESIGN-INTEGRATION.md +635 -0
- package/docs/QA.md +142 -0
- package/docs/SHARED-CORE.md +72 -0
- package/install +75 -0
- package/package.json +92 -4
- package/scripts/install.sh +324 -0
- package/scripts/uninstall.sh +91 -0
- package/src/allowance.ts +135 -0
- package/src/core/classify/classifier.ts +305 -0
- package/src/core/config-writer.ts +167 -0
- package/src/core/config.ts +1030 -0
- package/src/core/context/strategy.ts +188 -0
- package/src/core/context-health.ts +39 -0
- package/src/core/failures.ts +398 -0
- package/src/core/history/opencode.ts +173 -0
- package/src/core/models/protocol.ts +22 -0
- package/src/core/models/registry.ts +203 -0
- package/src/core/models/seed.ts +771 -0
- package/src/core/offline/mode.ts +321 -0
- package/src/core/plan-page.ts +390 -0
- package/src/core/plans/catalog.ts +481 -0
- package/src/core/policy/calibration.ts +259 -0
- package/src/core/policy/eligibility.ts +578 -0
- package/src/core/providers/enablement.ts +143 -0
- package/src/core/providers/opendesign-credits.ts +206 -0
- package/src/core/providers/opendesign-models.ts +628 -0
- package/src/core/providers/opendesign.ts +474 -0
- package/src/core/quota/calibration.ts +166 -0
- package/src/core/quota/commandcode.ts +471 -0
- package/src/core/quota/credentials.ts +490 -0
- package/src/core/quota/ledger.ts +1273 -0
- package/src/core/quota/opencode-go.ts +148 -0
- package/src/core/quota/opendesign.ts +1043 -0
- package/src/core/quota/reconcile.ts +155 -0
- package/src/core/quota/report.ts +261 -0
- package/src/core/quota/service.ts +273 -0
- package/src/core/reprompt.ts +48 -0
- package/src/core/routing/engine.ts +919 -0
- package/src/core/types.ts +825 -0
- package/src/core/ui.ts +236 -0
- package/src/core/util.ts +114 -0
- package/src/gui/index.tsx +1670 -0
- package/src/gui/viewmodel.ts +456 -0
- package/src/index.ts +48 -0
- package/src/rpc.ts +204 -0
- package/src/server/controller.ts +3281 -0
- package/src/server/route-tracker.ts +60 -0
- package/src/server/shared-core.ts +671 -0
- package/src/tui/index.tsx +168 -0
package/SECURITY.md
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Reporting a vulnerability
|
|
4
|
+
|
|
5
|
+
Open a private security advisory at
|
|
6
|
+
`https://github.com/qadanm/opencode-smart-router/security/advisories/new`, or
|
|
7
|
+
email the maintainer listed in `package.json`. Please don't open a public issue
|
|
8
|
+
for a suspected vulnerability.
|
|
9
|
+
|
|
10
|
+
Include what you can: the version or commit, your configuration, what you saw,
|
|
11
|
+
and the smallest reproduction. You'll get an acknowledgment within a few days.
|
|
12
|
+
This is a single-maintainer project with no paid support, so those dates are
|
|
13
|
+
intentions, not a contract.
|
|
14
|
+
|
|
15
|
+
## What this software does with credentials
|
|
16
|
+
|
|
17
|
+
A tool that reads credentials from disk owes you a precise account of which files
|
|
18
|
+
it touches, why, and what happens to the values.
|
|
19
|
+
|
|
20
|
+
### The short version
|
|
21
|
+
|
|
22
|
+
- The router reads **bearer credentials you already store on your own machine**,
|
|
23
|
+
for providers you already connected.
|
|
24
|
+
- It sends a credential **only** to that credential's own provider, in an
|
|
25
|
+
`Authorization` header, either to read usage or to make an inference request you
|
|
26
|
+
asked for.
|
|
27
|
+
- It **never** logs, prints, persists, or transmits a credential anywhere else.
|
|
28
|
+
- Automatic discovery from predictable paths is **off by default**.
|
|
29
|
+
|
|
30
|
+
### Discovery requires opt-in
|
|
31
|
+
|
|
32
|
+
Reading well-known credential files requires either:
|
|
33
|
+
|
|
34
|
+
```jsonc
|
|
35
|
+
// ~/.config/opencode/smart-router.json
|
|
36
|
+
{ "providers": { "commandcode": { "credentialSource": "auto" } } }
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
or the environment flag `SMART_ROUTER_ALLOW_KEY_DISCOVERY=1`.
|
|
40
|
+
|
|
41
|
+
Without one of those, the router reads only:
|
|
42
|
+
|
|
43
|
+
- credentials named by `credentialPath` in configuration, and
|
|
44
|
+
- credentials in the environment (`COMMAND_CODE_API_KEY`, `OPENDESIGN_API_KEY`)
|
|
45
|
+
or in the provider settings the host already holds.
|
|
46
|
+
|
|
47
|
+
`credentialSource: "off"` disables filesystem reads entirely.
|
|
48
|
+
|
|
49
|
+
### It does not read other applications' credential stores
|
|
50
|
+
|
|
51
|
+
Only paths belonging to this router's configuration directory or to the named
|
|
52
|
+
provider are considered. The router **does not read OpenCode's own `auth.json`**.
|
|
53
|
+
That file is another application's unified store: it holds OAuth material for
|
|
54
|
+
every provider you connected, and reading it would mean handling tokens the
|
|
55
|
+
router was never asked about.
|
|
56
|
+
|
|
57
|
+
If you find a code path that reads a foreign credential store, treat it as a
|
|
58
|
+
vulnerability and report it.
|
|
59
|
+
|
|
60
|
+
### Filesystem hardening
|
|
61
|
+
|
|
62
|
+
Each credential file is:
|
|
63
|
+
|
|
64
|
+
- opened **once**, with `O_NOFOLLOW`, so a symlink cannot redirect the read;
|
|
65
|
+
- checked against the **open descriptor** instead of a separate `stat` call,
|
|
66
|
+
which closes a time-of-check/time-of-use race where the path could be swapped
|
|
67
|
+
between the permission check and the read;
|
|
68
|
+
- required to be a **regular file** with **no group or world permission bits**.
|
|
69
|
+
Anything else is refused, because on a shared machine another account could
|
|
70
|
+
otherwise influence which credential the router picks;
|
|
71
|
+
- **capped at 64 KiB** before any read is attempted;
|
|
72
|
+
- always **closed**, on every path out of the function.
|
|
73
|
+
|
|
74
|
+
A file found by discovery must also be owned by the current user. See the next
|
|
75
|
+
section for why that check is narrower than it sounds.
|
|
76
|
+
|
|
77
|
+
#### Containers and bind mounts
|
|
78
|
+
|
|
79
|
+
The ownership check compares a **discovered** file's uid with `process.getuid()`.
|
|
80
|
+
Inside a container the process often runs as a different uid than the one that
|
|
81
|
+
owns the mounted file, so discovery refuses it. That is deliberate. When scanning
|
|
82
|
+
well-known locations, the router cannot tell "my own secret under another uid"
|
|
83
|
+
from "someone else's secret."
|
|
84
|
+
|
|
85
|
+
A file you **name** (`credentialPath`, `controlKeyPath`) skips the uid comparison,
|
|
86
|
+
because naming a path is already a deliberate trust decision. The group and world
|
|
87
|
+
permission check still applies to named files.
|
|
88
|
+
|
|
89
|
+
If discovery refuses a credential in a container, you have three options:
|
|
90
|
+
|
|
91
|
+
1. run the process as the file's owner (`--user $(id -u):$(id -g)`);
|
|
92
|
+
2. `chown` the mounted secret to the container's uid;
|
|
93
|
+
3. name the file with `credentialPath`, or pass the credential through the
|
|
94
|
+
environment (`COMMAND_CODE_API_KEY`, `OPENDESIGN_API_KEY`).
|
|
95
|
+
|
|
96
|
+
No configuration switch disables the permission check itself.
|
|
97
|
+
|
|
98
|
+
### Secrets do not reach output
|
|
99
|
+
|
|
100
|
+
- No credential is written to the ledger, a routing decision, a session title, a
|
|
101
|
+
diagnostic bundle, an event, or a log line.
|
|
102
|
+
- Error strings from a credential read pass through `redact()`.
|
|
103
|
+
- Diagnostic output identifies a credential source by a fixed label and, for
|
|
104
|
+
files, the **final path segment only**: `file (commandcode.key)`. Never a path
|
|
105
|
+
prefix, never a value. An unrecognized source string is reported as `configured`
|
|
106
|
+
instead of being echoed, so a caller cannot turn that field into an arbitrary
|
|
107
|
+
channel into a persisted note.
|
|
108
|
+
- Warnings *do* name the path that was rejected, for example "Ignoring Command
|
|
109
|
+
Code credential file with group/world permissions: /path/to/key". You need the
|
|
110
|
+
path to fix the permission. It is not a secret, but on a multi-user machine it
|
|
111
|
+
does reveal part of a directory layout. Keep credentials inside your home
|
|
112
|
+
directory if that matters.
|
|
113
|
+
- `test/credentials-security.test.ts` asserts each of these properties.
|
|
114
|
+
|
|
115
|
+
### The privileged control key
|
|
116
|
+
|
|
117
|
+
An OpenDesign account-control key can read billing state, so it is treated as a
|
|
118
|
+
privileged credential:
|
|
119
|
+
|
|
120
|
+
- it is **never** discovered from the environment or by scanning;
|
|
121
|
+
- it is read **only** when you name a file with `controlKeyPath`;
|
|
122
|
+
- the router never requests, generates, or acquires one.
|
|
123
|
+
|
|
124
|
+
### No spending
|
|
125
|
+
|
|
126
|
+
The router is subscription-only by default. It does not spend wallet balance,
|
|
127
|
+
does not enable automatic top-up, and does not fall back to pay-as-you-go pricing
|
|
128
|
+
unless `allowExtraUsage: true` is set explicitly.
|
|
129
|
+
|
|
130
|
+
## Data this software stores
|
|
131
|
+
|
|
132
|
+
The local ledger records, per request: timestamp, provider, model, token counts
|
|
133
|
+
(input, output, cache read, cache write), an estimated dollar cost, and an
|
|
134
|
+
outcome. It lives in the host's local database.
|
|
135
|
+
|
|
136
|
+
It does **not** store prompt text, file contents, model responses, or
|
|
137
|
+
credentials. Prompt text storage is a separate setting (`logging.storePromptText`)
|
|
138
|
+
that is off by default and that this project does not recommend turning on.
|
|
139
|
+
|
|
140
|
+
Aged usage events fold into hourly aggregates with the same fields, so totals
|
|
141
|
+
survive without keeping per-request detail forever.
|
|
142
|
+
|
|
143
|
+
## Network endpoints
|
|
144
|
+
|
|
145
|
+
The router contacts only:
|
|
146
|
+
|
|
147
|
+
| Purpose | Host |
|
|
148
|
+
|---|---|
|
|
149
|
+
| Model inference | whichever provider the host selected |
|
|
150
|
+
| OpenCode Go usage | `opencode.ai` |
|
|
151
|
+
| Command Code usage | `api.commandcode.ai` (read-only `GET` on undocumented alpha routes) |
|
|
152
|
+
| OpenDesign usage | the configured gateway host (read-only, only with a control key) |
|
|
153
|
+
|
|
154
|
+
There is no telemetry, no analytics endpoint, and no update check. Two
|
|
155
|
+
qualifications on that:
|
|
156
|
+
|
|
157
|
+
- **`status` and `usage` make no inference call.** They spend no tokens and cost
|
|
158
|
+
you nothing.
|
|
159
|
+
- **`status` may start a background quota probe** if a cached snapshot is older
|
|
160
|
+
than `quota.probeIntervalMinutes` (5 by default). The probe is a `GET` to a
|
|
161
|
+
provider's usage endpoint. It runs asynchronously and the command does not wait
|
|
162
|
+
for it. With `probeUsage: false` on a provider, no probe happens.
|
|
163
|
+
|
|
164
|
+
## Supply chain
|
|
165
|
+
|
|
166
|
+
The runtime has one dependency, `@opencode/plugin`, which is the host's own
|
|
167
|
+
plugin SDK. The portable allowance core
|
|
168
|
+
(`@qadanm/opencode-smart-router/allowance`) bundles to a single file with **zero
|
|
169
|
+
external imports**, and a test enforces that it imports no Node built-ins and no
|
|
170
|
+
host modules.
|
|
171
|
+
|
|
172
|
+
## Supported versions
|
|
173
|
+
|
|
174
|
+
This is pre-1.0 software. Security fixes land on the latest version only.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"enabled": true,
|
|
3
|
+
"mode": "balanced",
|
|
4
|
+
"dryRun": false,
|
|
5
|
+
"providers": {
|
|
6
|
+
"opencode-go": {
|
|
7
|
+
"enabled": true,
|
|
8
|
+
"plan": "go",
|
|
9
|
+
"reserveMonthlyPct": 10,
|
|
10
|
+
"reserveWeeklyPct": 15,
|
|
11
|
+
"reserveFiveHourPct": 10,
|
|
12
|
+
"allowExtraUsage": false,
|
|
13
|
+
"probeUsage": true,
|
|
14
|
+
"credentialSource": "auto"
|
|
15
|
+
},
|
|
16
|
+
"commandcode": {
|
|
17
|
+
"enabled": true,
|
|
18
|
+
"plan": "goat",
|
|
19
|
+
"reserveMonthlyPct": 10,
|
|
20
|
+
"reserveWeeklyPct": 15,
|
|
21
|
+
"reserveFiveHourPct": 10,
|
|
22
|
+
"allowExtraUsage": false,
|
|
23
|
+
"probeUsage": true,
|
|
24
|
+
"monthlyCreditsUsd": 70,
|
|
25
|
+
"fiveHourCapUsd": 14,
|
|
26
|
+
"weeklyCapUsd": 35,
|
|
27
|
+
"credentialSource": "auto"
|
|
28
|
+
},
|
|
29
|
+
"opendesign": {
|
|
30
|
+
"enabled": true,
|
|
31
|
+
"plan": "pro",
|
|
32
|
+
"reserveMonthlyPct": 5,
|
|
33
|
+
"reserveWeeklyPct": 10,
|
|
34
|
+
"reserveFiveHourPct": 0,
|
|
35
|
+
"allowExtraUsage": false,
|
|
36
|
+
"probeUsage": true,
|
|
37
|
+
"subscriptionOnly": true,
|
|
38
|
+
"providerScopedQuota": true,
|
|
39
|
+
"ignoreFiveHourWindow": true,
|
|
40
|
+
"credentialSource": "auto"
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"privacy": {
|
|
44
|
+
"allowTrainingModels": false,
|
|
45
|
+
"allowFreeModels": false,
|
|
46
|
+
"requireZdr": false
|
|
47
|
+
},
|
|
48
|
+
"premium": {
|
|
49
|
+
"mode": "ask",
|
|
50
|
+
"monthlyBudgetUsd": 0,
|
|
51
|
+
"oneShotApproval": true
|
|
52
|
+
},
|
|
53
|
+
"routing": {
|
|
54
|
+
"intelligenceFloor": 39.5,
|
|
55
|
+
"designFloor": 60,
|
|
56
|
+
"sophisticatedDesignFloor": 75,
|
|
57
|
+
"switchHysteresisPct": 20,
|
|
58
|
+
"largeContextTokens": 300000,
|
|
59
|
+
"largeContextHysteresisPct": 35,
|
|
60
|
+
"maxEscalationsPerTask": 2,
|
|
61
|
+
"preferSameProvider": true,
|
|
62
|
+
"failClosed": true,
|
|
63
|
+
"protectEscalationPool": true,
|
|
64
|
+
"longContextFloorTokens": 1000000
|
|
65
|
+
},
|
|
66
|
+
"quota": {
|
|
67
|
+
"probeOpenCodeGo": true,
|
|
68
|
+
"probeCommandCode": true,
|
|
69
|
+
"probeOpenDesign": true,
|
|
70
|
+
"probeIntervalMinutes": 5,
|
|
71
|
+
"bootstrapSessions": 100,
|
|
72
|
+
"bootstrapDays": 31
|
|
73
|
+
},
|
|
74
|
+
"contextHealth": {
|
|
75
|
+
"enabled": true,
|
|
76
|
+
"warnAtTokens": 400000,
|
|
77
|
+
"recommendAtTokens": 700000
|
|
78
|
+
},
|
|
79
|
+
"contextManagement": {
|
|
80
|
+
"mode": "native",
|
|
81
|
+
"enabled": true,
|
|
82
|
+
"compactAtTokens": 0,
|
|
83
|
+
"cooldownMinutes": 10,
|
|
84
|
+
"allowEmergencyCompaction": true
|
|
85
|
+
},
|
|
86
|
+
"speed": {
|
|
87
|
+
"enabled": true,
|
|
88
|
+
"weightPct": 15,
|
|
89
|
+
"minSamples": 3,
|
|
90
|
+
"slowRouteMultiple": 2.5
|
|
91
|
+
},
|
|
92
|
+
"ui": {
|
|
93
|
+
"titleIndicator": false,
|
|
94
|
+
"notifications": true,
|
|
95
|
+
"compactMode": false
|
|
96
|
+
},
|
|
97
|
+
"logging": {
|
|
98
|
+
"storePromptText": false,
|
|
99
|
+
"decisionLogSize": 100,
|
|
100
|
+
"diagnostics": true
|
|
101
|
+
},
|
|
102
|
+
"models": {
|
|
103
|
+
"approved": [],
|
|
104
|
+
"excluded": [],
|
|
105
|
+
"tierOverrides": {},
|
|
106
|
+
"definitions": []
|
|
107
|
+
}
|
|
108
|
+
}
|