dsh-model 0.0.1 → 0.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/README.en.md +127 -0
- package/README.md +96 -43
- package/bin/dsh-model.js +8 -0
- package/dist/bridge/aggregate.js +91 -0
- package/dist/bridge/login.js +127 -0
- package/dist/bridge/runtime.js +136 -0
- package/dist/bridge/server.js +183 -0
- package/dist/bridge/workbuddy/_compat.js +52 -0
- package/dist/bridge/workbuddy/app-version.js +141 -0
- package/dist/bridge/workbuddy/auth.js +548 -0
- package/dist/bridge/workbuddy/catalog.js +154 -0
- package/dist/bridge/workbuddy/client-identity.js +234 -0
- package/dist/bridge/workbuddy/desktop-credential-protection.js +992 -0
- package/dist/bridge/workbuddy/loopback.js +52 -0
- package/dist/bridge/workbuddy/probe.js +153 -0
- package/dist/bridge/workbuddy/status-paths.js +49 -0
- package/dist/bridge/workbuddy/upstream.js +1180 -0
- package/dist/bridge/workbuddy/variants.js +101 -0
- package/dist/cli.js +215 -0
- package/dist/commands/bridge.js +68 -0
- package/dist/commands/connect.js +67 -0
- package/dist/commands/engine.js +122 -0
- package/dist/commands/key.js +75 -0
- package/dist/commands/login.js +113 -0
- package/dist/commands/logs.js +19 -0
- package/dist/commands/opencode.js +83 -0
- package/dist/commands/remote.js +129 -0
- package/dist/commands/self.js +116 -0
- package/dist/commands/service.js +72 -0
- package/dist/commands/setup.js +219 -0
- package/dist/commands/sources.js +196 -0
- package/dist/commands/status.js +216 -0
- package/dist/commands/uninstall.js +97 -0
- package/dist/commands/workbuddy.js +117 -0
- package/dist/context.js +86 -0
- package/dist/daemon/client.js +27 -0
- package/dist/daemon/core.js +850 -0
- package/dist/daemon/service.js +33 -0
- package/dist/daemon/stats.js +126 -0
- package/dist/daemon/usage.js +213 -0
- package/dist/dsh/cli.js +90 -0
- package/dist/dsh/connect.js +257 -0
- package/dist/dsh/credentials.js +55 -0
- package/dist/dsh/locate.js +129 -0
- package/dist/dsh/patch.js +106 -0
- package/dist/dsh/pick.js +59 -0
- package/dist/engine/auth.js +40 -0
- package/dist/engine/client.js +104 -0
- package/dist/engine/compat.js +94 -0
- package/dist/engine/config.js +47 -0
- package/dist/engine/install.js +118 -0
- package/dist/engine/mgmt.js +99 -0
- package/dist/engine/sourcemap.js +48 -0
- package/dist/errors.js +14 -0
- package/dist/i18n.js +39 -0
- package/dist/integrations/dshplugin.js +89 -0
- package/dist/integrations/opencode.js +101 -0
- package/dist/integrations/workbuddy.js +114 -0
- package/dist/keys.js +54 -0
- package/dist/ops.js +178 -0
- package/dist/remote/public.js +109 -0
- package/dist/secrets.js +13 -0
- package/dist/service/index.js +48 -0
- package/dist/service/launchd.js +89 -0
- package/dist/service/systemd.js +83 -0
- package/dist/sources.js +36 -0
- package/dist/state.js +90 -0
- package/dist/upstreams.js +23 -0
- package/dist/util/diff.js +30 -0
- package/dist/util/exec.js +42 -0
- package/dist/util/fs.js +107 -0
- package/dist/util/output.js +50 -0
- package/dist/util/pkg.js +13 -0
- package/dist/util/port.js +16 -0
- package/dist/util/prompt.js +40 -0
- package/dist/util/proxy.js +73 -0
- package/dist/util/redact.js +7 -0
- package/engine-manifest.json +28 -0
- package/package.json +77 -7
- package/plugin/client.js +688 -0
- package/plugin/cordis.patch.yml +4 -0
- package/plugin/index.js +231 -0
- package/docs/DESIGN.md +0 -610
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
// Ported from dsh-workbuddy-connect v0.7.1 (commit 390582e04da4e8e8bf746d71997440e4b716ec47),
|
|
2
|
+
// https://github.com/corrinehu/dsh-workbuddy-connect — MIT License, Copyright (c) 2026 Corrine Hu.
|
|
3
|
+
// See LICENSE.dsh-workbuddy-connect in this directory. Changes: imports adapted to dsh-model (./_compat.js).
|
|
4
|
+
/**
|
|
5
|
+
* Shared loopback gates for the plugin's local HTTP surfaces: the loopback
|
|
6
|
+
* shim and the same-origin web-status route. Both are only ever meant to be
|
|
7
|
+
* addressed through the machine's loopback interface.
|
|
8
|
+
*
|
|
9
|
+
* @module dsh-workbuddy-connect/loopback
|
|
10
|
+
*/
|
|
11
|
+
/** Loopback hostnames a local plugin surface may be addressed by. */
|
|
12
|
+
export const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '[::1]']);
|
|
13
|
+
/** Strip the optional :port from a Host header value, IPv6-bracket aware. */
|
|
14
|
+
export function hostnameOfHost(host) {
|
|
15
|
+
let hostname = host.trim().toLowerCase();
|
|
16
|
+
if (hostname.startsWith('[')) {
|
|
17
|
+
const end = hostname.indexOf(']');
|
|
18
|
+
return end === -1 ? hostname : hostname.slice(0, end + 1);
|
|
19
|
+
}
|
|
20
|
+
// Only `name:port` with a single colon is a port; anything with more colons
|
|
21
|
+
// is an (unbracketed) IPv6 literal and must not be truncated.
|
|
22
|
+
const colon = hostname.lastIndexOf(':');
|
|
23
|
+
if (colon !== -1 && !hostname.slice(0, colon).includes(':') && /^\d+$/.test(hostname.slice(colon + 1))) {
|
|
24
|
+
hostname = hostname.slice(0, colon);
|
|
25
|
+
}
|
|
26
|
+
return hostname;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* The request's Host header must name the loopback interface. A DNS-rebinding
|
|
30
|
+
* page (attacker domain re-resolved to 127.0.0.1) sends its own domain in
|
|
31
|
+
* Host, so this check drops those before any routing happens.
|
|
32
|
+
*/
|
|
33
|
+
export function hostIsLoopback(host) {
|
|
34
|
+
if (host === undefined || host.trim() === '')
|
|
35
|
+
return false;
|
|
36
|
+
return LOOPBACK_HOSTS.has(hostnameOfHost(host));
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A browser-sent Origin (present header) must be loopback. Non-browser
|
|
40
|
+
* clients (the plugin's own fetch calls) send no Origin at all and pass.
|
|
41
|
+
*/
|
|
42
|
+
export function originIsLoopback(origin) {
|
|
43
|
+
if (origin === undefined || origin.trim() === '')
|
|
44
|
+
return true;
|
|
45
|
+
try {
|
|
46
|
+
const { hostname } = new URL(origin);
|
|
47
|
+
return LOOPBACK_HOSTS.has(hostname) || hostname === '::1';
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// Ported from dsh-workbuddy-connect v0.7.1 (commit 390582e04da4e8e8bf746d71997440e4b716ec47),
|
|
2
|
+
// https://github.com/corrinehu/dsh-workbuddy-connect — MIT License, Copyright (c) 2026 Corrine Hu.
|
|
3
|
+
// See LICENSE.dsh-workbuddy-connect in this directory. Changes: imports adapted to dsh-model (./_compat.js).
|
|
4
|
+
/**
|
|
5
|
+
* The reasoning-effort probe: decide whether a model's `reasoning_effort`
|
|
6
|
+
* parameter is actually validated, and if so which canonical values it accepts.
|
|
7
|
+
*
|
|
8
|
+
* Implements `docs/reasoning-effort-probe-plan.md` §4. The order matters and is
|
|
9
|
+
* not an optimization:
|
|
10
|
+
*
|
|
11
|
+
* 1. **Baseline** (no `reasoning_effort`) proves the model, credential, and
|
|
12
|
+
* request shape work at all, so a later rejection can be attributed.
|
|
13
|
+
* 2. **Sentinel** (a fresh random, impossible-to-collide value) answers the one
|
|
14
|
+
* question a per-level sweep cannot: does the upstream validate the field?
|
|
15
|
+
* A model that accepts the sentinel answers 200 to *everything*, so its
|
|
16
|
+
* per-level results would be uniformly false positives.
|
|
17
|
+
* 3. **Levels**, only after the sentinel was refused.
|
|
18
|
+
*
|
|
19
|
+
* The result is an observation, never a capability claim. Even a fully
|
|
20
|
+
* successful sweep means "the upstream accepted these spellings", not "these
|
|
21
|
+
* spellings change how the model thinks".
|
|
22
|
+
*
|
|
23
|
+
* @module dsh-workbuddy-connect/probe
|
|
24
|
+
*/
|
|
25
|
+
import { randomBytes } from 'node:crypto';
|
|
26
|
+
/**
|
|
27
|
+
* The canonical values a probe tests, in a fixed order.
|
|
28
|
+
*
|
|
29
|
+
* `minimal` is absent: it appears in no upstream vocabulary. `off` is absent
|
|
30
|
+
* by policy — disabling thinking is a separate capability the upstream must
|
|
31
|
+
* declare through `canDisableThinking`, never something probing may infer.
|
|
32
|
+
*/
|
|
33
|
+
export const PROBE_EFFORT_CANDIDATES = ['low', 'medium', 'high', 'xhigh', 'max'];
|
|
34
|
+
/** Prompt body used by every probe request; carries nothing user-specific. */
|
|
35
|
+
export const PROBE_PROMPT = 'ping';
|
|
36
|
+
/** Output ceiling for a probe request; the answer itself is never read. */
|
|
37
|
+
export const PROBE_MAX_TOKENS = 1;
|
|
38
|
+
/** Per-request ceiling so one hung probe cannot stall the queue. */
|
|
39
|
+
export const PROBE_REQUEST_TIMEOUT_MS = 30_000;
|
|
40
|
+
/** Default sentinel: unmistakably non-canonical, different on every call. */
|
|
41
|
+
export function randomSentinel() {
|
|
42
|
+
return `probe_sentinel_${randomBytes(12).toString('hex')}`;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* The upstream's "this effort value is not supported" codes, per region, as
|
|
46
|
+
* measured on each live endpoint. The sets are kept separate so a code only
|
|
47
|
+
* ever widens detection for the endpoint it was measured on.
|
|
48
|
+
*
|
|
49
|
+
* - `invalid_reasoning_effort` — the China endpoint, measured 2026-09-11
|
|
50
|
+
* (plan §4.2). Also kept for `global` as a fallback spelling.
|
|
51
|
+
* - `model_param_invalid` — the global endpoint, measured 2026-10-01. A
|
|
52
|
+
* non-canonical value is answered `400` / `11133` with this code, which names
|
|
53
|
+
* no field (`extError.param` is empty), instead of one that names the effort.
|
|
54
|
+
* It is generic enough to be readable here only because the baseline step has
|
|
55
|
+
* already proved the *same* request without `reasoning_effort` succeeds,
|
|
56
|
+
* leaving the sentinel as the only difference between the two attempts. A
|
|
57
|
+
* sibling code in the same `11133` envelope that names another parameter
|
|
58
|
+
* (`integer_below_min_value`, the `max_tokens` floor) and a body carrying no
|
|
59
|
+
* `extError` at all (`11102`, unknown model) are therefore not mistaken for
|
|
60
|
+
* it, and neither is a level-sweep answer. It is *not* added to `cn`: that
|
|
61
|
+
* endpoint was measured answering the specific code, and reading a generic
|
|
62
|
+
* code there would widen attribution beyond what was observed.
|
|
63
|
+
*
|
|
64
|
+
* Neither is treated as a permanent protocol promise: anything unrecognized
|
|
65
|
+
* still degrades to `unknown` rather than to a capability conclusion.
|
|
66
|
+
*/
|
|
67
|
+
const INVALID_EFFORT_CODES = {
|
|
68
|
+
cn: new Set(['invalid_reasoning_effort']),
|
|
69
|
+
global: new Set(['invalid_reasoning_effort', 'model_param_invalid']),
|
|
70
|
+
};
|
|
71
|
+
/** Whether an attempt is an attributable rejection of the effort value. */
|
|
72
|
+
function isEffortRejection(attempt, region) {
|
|
73
|
+
const codes = INVALID_EFFORT_CODES[region];
|
|
74
|
+
return attempt.status === 400 && attempt.errorCode !== undefined && codes.has(attempt.errorCode);
|
|
75
|
+
}
|
|
76
|
+
/** Whether an attempt shows the upstream accepted the request and streamed. */
|
|
77
|
+
function isAcceptance(attempt) {
|
|
78
|
+
return attempt.status === 200 && attempt.streamed;
|
|
79
|
+
}
|
|
80
|
+
/** Why an attempt ended in `unknown`, phrased for a log line. */
|
|
81
|
+
function unknownReason(stage, attempt) {
|
|
82
|
+
const code = attempt.errorCode === undefined ? '' : ` (${attempt.errorCode})`;
|
|
83
|
+
const detail = attempt.detail === undefined ? '' : `: ${attempt.detail}`;
|
|
84
|
+
return `${stage} status ${attempt.status}${code}${detail}`;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Probe one model.
|
|
88
|
+
*
|
|
89
|
+
* `options.candidates` exists so tests can shorten the sweep; production always
|
|
90
|
+
* uses {@link PROBE_EFFORT_CANDIDATES}. `options.region` selects which
|
|
91
|
+
* endpoint's rejection vocabulary is read; it defaults to `cn`, which is also
|
|
92
|
+
* the production default for the China app.
|
|
93
|
+
*/
|
|
94
|
+
export async function probeModel(options) {
|
|
95
|
+
const sentinel = options.sentinel ?? randomSentinel;
|
|
96
|
+
const candidates = options.candidates ?? PROBE_EFFORT_CANDIDATES;
|
|
97
|
+
const timeoutMs = options.timeoutMs ?? PROBE_REQUEST_TIMEOUT_MS;
|
|
98
|
+
const region = options.region ?? 'cn';
|
|
99
|
+
let requests = 0;
|
|
100
|
+
const attempt = async (effort) => {
|
|
101
|
+
requests += 1;
|
|
102
|
+
const controller = new AbortController();
|
|
103
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
104
|
+
try {
|
|
105
|
+
return await options.send(effort, controller.signal);
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
return { status: 0, streamed: false, detail: `transport error: ${String(error)}` };
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
clearTimeout(timer);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
// 1. Baseline: proves the request path itself is sound. Without this a 400
|
|
115
|
+
// on the sentinel could just as well be an unrelated request problem.
|
|
116
|
+
const baseline = await attempt(undefined);
|
|
117
|
+
if (!isAcceptance(baseline)) {
|
|
118
|
+
return { validation: 'unknown', efforts: [], requests, reason: unknownReason('baseline', baseline) };
|
|
119
|
+
}
|
|
120
|
+
// 2. Sentinel: the only step that can distinguish "validates the field" from
|
|
121
|
+
// "accepts anything".
|
|
122
|
+
const probe = sentinel();
|
|
123
|
+
const sentinelAttempt = await attempt(probe);
|
|
124
|
+
if (isAcceptance(sentinelAttempt)) {
|
|
125
|
+
// The upstream took a value that cannot exist. Any per-level acceptance
|
|
126
|
+
// would be equally meaningless, so stop here rather than spend more
|
|
127
|
+
// requests producing false positives.
|
|
128
|
+
return { validation: 'non-validating', efforts: [], requests };
|
|
129
|
+
}
|
|
130
|
+
if (!isEffortRejection(sentinelAttempt, region)) {
|
|
131
|
+
return { validation: 'unknown', efforts: [], requests, reason: unknownReason('sentinel', sentinelAttempt) };
|
|
132
|
+
}
|
|
133
|
+
// 3. Levels: meaningful only because the sentinel was refused.
|
|
134
|
+
const accepted = [];
|
|
135
|
+
for (const effort of candidates) {
|
|
136
|
+
const levelAttempt = await attempt(effort);
|
|
137
|
+
if (isAcceptance(levelAttempt)) {
|
|
138
|
+
accepted.push(effort);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (isEffortRejection(levelAttempt, region))
|
|
142
|
+
continue;
|
|
143
|
+
// A non-decisive answer mid-sweep: the partial list is not a finding, so
|
|
144
|
+
// report the whole run as unknown rather than under-claiming a model.
|
|
145
|
+
return {
|
|
146
|
+
validation: 'unknown',
|
|
147
|
+
efforts: [],
|
|
148
|
+
requests,
|
|
149
|
+
reason: unknownReason(`level ${effort}`, levelAttempt),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
return { validation: 'validating', efforts: accepted, requests };
|
|
153
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// Ported from dsh-workbuddy-connect v0.7.1 (commit 390582e04da4e8e8bf746d71997440e4b716ec47),
|
|
2
|
+
// https://github.com/corrinehu/dsh-workbuddy-connect — MIT License, Copyright (c) 2026 Corrine Hu.
|
|
3
|
+
// See LICENSE.dsh-workbuddy-connect in this directory. Changes: imports adapted to dsh-model (./_compat.js).
|
|
4
|
+
/** Node-free constants and types shared by the Host and browser halves. */
|
|
5
|
+
/** Plugin-owned status endpoint consumed by its browser half. */
|
|
6
|
+
export const WORKBUDDY_STATUS_PATH = '/plugins/dsh-workbuddy-connect/status';
|
|
7
|
+
/**
|
|
8
|
+
* Plugin-owned probe control endpoint.
|
|
9
|
+
*
|
|
10
|
+
* Separate from the status route because it accepts writes: the status route's
|
|
11
|
+
* loopback Host/Origin guard protects against a DNS-rebinding *page*, which is
|
|
12
|
+
* not the same as authorizing a state-changing action. This route therefore
|
|
13
|
+
* also requires the in-process key the browser half receives with the status
|
|
14
|
+
* document.
|
|
15
|
+
*/
|
|
16
|
+
export const WORKBUDDY_PROBE_PATH = '/plugins/dsh-workbuddy-connect/probe';
|
|
17
|
+
/**
|
|
18
|
+
* The international (WorkBuddy AI) variant's own pair of routes.
|
|
19
|
+
*
|
|
20
|
+
* Kept as separate constants rather than a computed suffix so both halves
|
|
21
|
+
* reference literal strings: the browser bundle and the host bundle are built
|
|
22
|
+
* independently, and a shared expression is one build-config drift away from
|
|
23
|
+
* the desk asking a route the host never mounted.
|
|
24
|
+
*/
|
|
25
|
+
export const WORKBUDDY_AI_STATUS_PATH = '/plugins/dsh-workbuddy-connect/ai/status';
|
|
26
|
+
export const WORKBUDDY_AI_PROBE_PATH = '/plugins/dsh-workbuddy-connect/ai/probe';
|
|
27
|
+
/**
|
|
28
|
+
* Same-origin route backing the browser's update reminder.
|
|
29
|
+
*
|
|
30
|
+
* Read-only like the status route, so the same loopback Host/Origin gate
|
|
31
|
+
* applies; it answers public npm/GitHub metadata only and never token
|
|
32
|
+
* material.
|
|
33
|
+
*/
|
|
34
|
+
export const WORKBUDDY_UPDATE_PATH = '/plugins/dsh-workbuddy-connect/update';
|
|
35
|
+
/** Every reason code, for validation without trusting a wire value. */
|
|
36
|
+
const SIGNED_OUT_REASON_CODES = [
|
|
37
|
+
'no-credential',
|
|
38
|
+
'credential-region-mismatch',
|
|
39
|
+
'encrypted-credential-unreadable',
|
|
40
|
+
'electron-binary-not-found',
|
|
41
|
+
'electron-binary-ambiguous',
|
|
42
|
+
'electron-binary-unavailable',
|
|
43
|
+
'electron-path-invalid',
|
|
44
|
+
'electron-discovery-incomplete',
|
|
45
|
+
];
|
|
46
|
+
/** Whether a value is one of the closed set of signed-out reason codes. */
|
|
47
|
+
export function isWorkBuddySignedOutReasonCode(value) {
|
|
48
|
+
return typeof value === 'string' && SIGNED_OUT_REASON_CODES.includes(value);
|
|
49
|
+
}
|