@magpie-community/opencode-workbuddy-auth 0.2.1 → 0.2.3
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 +27 -15
- package/index.mjs +126 -5
- package/package.json +16 -4
package/README.md
CHANGED
|
@@ -174,21 +174,33 @@ default one, as the app does.
|
|
|
174
174
|
|
|
175
175
|
WorkBuddy refuses some agents' identity lines as an "unapproved channel"
|
|
176
176
|
(11128), so those lines are reworded before a chat goes out. Since 0.1.27
|
|
177
|
-
(#279) that includes DeepSeek Harness's opening line.
|
|
178
|
-
not reworded yet is
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
177
|
+
(#279) that includes DeepSeek Harness's opening line. Since 0.2.3 a refusal
|
|
178
|
+
of words not reworded yet is answered first by saying the same line with one
|
|
179
|
+
more comma — a vendor matches such a line whole and exactly, so the comma
|
|
180
|
+
passes it while the line still names the harness (measured 2026-10-30; a word
|
|
181
|
+
added after the product name did not pass, the comma did). The comma is set in
|
|
182
|
+
the system and assistant messages only — the two WorkBuddy reads — so the
|
|
183
|
+
user's own words and tools' results go out as they were sent. A system prompt
|
|
184
|
+
whose comma passed is remembered while the plugin is loaded, as a moved one
|
|
185
|
+
is, and its next chats go with the comma already in before the first send:
|
|
186
|
+
otherwise every turn, and every step of a tool loop, would pay the refusal
|
|
187
|
+
once. Only a chat refused still is sent once more, since 0.1.29, with the
|
|
188
|
+
system prompt moved into the first user message (inside `<instructions>` tags)
|
|
189
|
+
and WorkBuddy's default system prompt in its place: WorkBuddy checks only the
|
|
190
|
+
system and assistant messages, and the line it matched is then in neither. A system message later
|
|
191
|
+
in the chat becomes a user message where it stands. The plugin remembers each
|
|
192
|
+
refused system prompt while it is loaded, so that agent's next chats go moved
|
|
193
|
+
at once. The move can't reach an assistant message, so words the model quotes
|
|
194
|
+
back into one keep that chat refused until they are in the list reworded in
|
|
195
|
+
every message (since 0.1.30, the "Web GUI whose URL the runtime context names"
|
|
196
|
+
line, #291); otherwise only a new chat clears it. A refusal that stands after
|
|
197
|
+
the move ends in a hint that names no one client (since 0.1.28): WorkBuddy
|
|
198
|
+
refused words in this client's prompt; use it from Hermes, OpenCode or Pi, or
|
|
199
|
+
add another provider to the group. Since 0.2.3 that hint is preceded by the
|
|
200
|
+
address of [REFUSED.md](REFUSED.md), which says what the check reads, how a
|
|
201
|
+
line is reworded to pass it, and how to turn such a line off in an agent that
|
|
202
|
+
has a switch for it — so the error carries its own fix, for the person who
|
|
203
|
+
reads it or the agent they paste it to. Neither refusal rests the account.
|
|
192
204
|
|
|
193
205
|
The loader also translates OpenAI-compatible request spellings WorkBuddy
|
|
194
206
|
doesn't take: `max_completion_tokens` becomes `max_tokens`, `developer`
|
package/index.mjs
CHANGED
|
@@ -819,8 +819,14 @@ const FLAGGED = [
|
|
|
819
819
|
[/(?<!the )Codex CLI is an open source project led by OpenAI\./gi, "The Codex CLI is an open source project led by OpenAI."],
|
|
820
820
|
// DeepSeek Harness (DSH) opens its system prompt with this line, which
|
|
821
821
|
// WorkBuddy refuses as 11128 (magpie-community/plugins#279; the same
|
|
822
|
-
// refusal traced in smart-open/TraeWorkAssistant#125).
|
|
823
|
-
|
|
822
|
+
// refusal traced in smart-open/TraeWorkAssistant#125). WorkBuddy matches
|
|
823
|
+
// the line whole and exactly, so a comma is enough to pass: measured on
|
|
824
|
+
// 2026-10-30 against copilot.tencent.com, one account, streamed, the line
|
|
825
|
+
// as DSH writes it answered 400 code 11128 and the same line with the
|
|
826
|
+
// comma answered 200. Saying it in another agent's terms passed too, but
|
|
827
|
+
// took the harness's own name out of the only line that says which agent
|
|
828
|
+
// runs here, and that name is what the rest of the prompt refers back to.
|
|
829
|
+
[/You are an AI agent powered by DeepSeek Harness\.?/gi, "You are an AI agent, powered by DeepSeek Harness."],
|
|
824
830
|
// A line WorkBuddy refuses in a long DeepSeek Harness chat, measured 400
|
|
825
831
|
// then 200 with this wording by tearslee (magpie-community/plugins#291). The
|
|
826
832
|
// model can quote it back into an assistant message, which the move to
|
|
@@ -1002,6 +1008,79 @@ function systemAsUserOn(b) {
|
|
|
1002
1008
|
return true
|
|
1003
1009
|
}
|
|
1004
1010
|
|
|
1011
|
+
// OPENERS are the shapes in which an agent's opening says which harness it
|
|
1012
|
+
// runs in, as a vendor may have fingerprinted one. A vendor matches a refused
|
|
1013
|
+
// line whole and exactly — one extra space passes it, as FLAGGED's note above
|
|
1014
|
+
// has it — so the same line with one more comma passes while still saying
|
|
1015
|
+
// what it said. Measured 2026-10-30 against copilot.tencent.com, one account,
|
|
1016
|
+
// streamed, deepseek-v4.1-flash at max: DeepSeek Harness's opener answered
|
|
1017
|
+
// 400 code 11128 as written, and 200 with the comma, the harness's name still
|
|
1018
|
+
// in it; a suffix in its place (…Harness Desktop., …Harness CLI.) changed
|
|
1019
|
+
// nothing, so it is the line's shape the vendor knows, not the product alone.
|
|
1020
|
+
const OPENERS = [
|
|
1021
|
+
[/(\bYou are (?:an?|the)\s[^\n.,]{0,60}?\b(?:agent|assistant))\s+(powered by|running on|running in|built on|based on)\b/gi, "$1, $2"],
|
|
1022
|
+
]
|
|
1023
|
+
|
|
1024
|
+
function opened(text) {
|
|
1025
|
+
for (const [re, to] of OPENERS) text = text.replace(re, to)
|
|
1026
|
+
return text
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
// commaPaused is body with OPENERS' comma set in its system and assistant
|
|
1030
|
+
// messages — the two WorkBuddy reads (REFUSED.md). The user's messages and
|
|
1031
|
+
// tools' results are not among them: a comma in a message the vendor never
|
|
1032
|
+
// matches changes what someone said without changing what was refused, and
|
|
1033
|
+
// the user's own words come back altered. It is tried before systemAsUser,
|
|
1034
|
+
// which takes the system prompt out of the slot an agent's instructions are
|
|
1035
|
+
// read in. null when there was nothing to set a comma in.
|
|
1036
|
+
//
|
|
1037
|
+
// The two roles are read off the message's own role: a message with no role,
|
|
1038
|
+
// or one of another kind, is left as it is. roles, when given, collects the
|
|
1039
|
+
// roles the comma went into — the caller uses it to tell a comma that made a
|
|
1040
|
+
// chat pass in its system prompt from one that only set an assistant
|
|
1041
|
+
// message's (commaSystems).
|
|
1042
|
+
const PAUSED_ROLES = new Set(["system", "assistant"])
|
|
1043
|
+
|
|
1044
|
+
function commaPaused(body, roles) {
|
|
1045
|
+
const b = chatOf(body)
|
|
1046
|
+
return commaPausedOn(b, roles) ? JSON.stringify(b) : null
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
// commaPausedOn makes commaPaused's change on a chat's object, in place; false
|
|
1050
|
+
// when there was nothing to set a comma in. commaPaused is this on a body, and
|
|
1051
|
+
// the first send of a system prompt whose comma passed is this too
|
|
1052
|
+
// (commaSystems), so the two can never drift apart.
|
|
1053
|
+
function commaPausedOn(b, roles) {
|
|
1054
|
+
const msgs = b?.messages
|
|
1055
|
+
if (!Array.isArray(msgs)) return false
|
|
1056
|
+
let changed = false
|
|
1057
|
+
for (const m of msgs) {
|
|
1058
|
+
if (!PAUSED_ROLES.has(m?.role)) continue
|
|
1059
|
+
let mine = false
|
|
1060
|
+
if (typeof m.content === "string") {
|
|
1061
|
+
const t = opened(m.content)
|
|
1062
|
+
if (t !== m.content) {
|
|
1063
|
+
m.content = t
|
|
1064
|
+
mine = true
|
|
1065
|
+
}
|
|
1066
|
+
} else if (Array.isArray(m.content)) {
|
|
1067
|
+
for (const p of m.content) {
|
|
1068
|
+
if (typeof p?.text !== "string") continue
|
|
1069
|
+
const t = opened(p.text)
|
|
1070
|
+
if (t === p.text) continue
|
|
1071
|
+
p.text = t
|
|
1072
|
+
mine = true
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
// a message whose parts held no opening of their own is not changed, and
|
|
1076
|
+
// is not reported as one that was
|
|
1077
|
+
if (!mine) continue
|
|
1078
|
+
changed = true
|
|
1079
|
+
roles?.add(m.role)
|
|
1080
|
+
}
|
|
1081
|
+
return changed
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1005
1084
|
// normalizeToolChoice converts the object form emitted by OpenAI clients to
|
|
1006
1085
|
// WorkBuddy's string form. It runs after fitted(), so a named choice follows a
|
|
1007
1086
|
// tool name that was made safe for WorkBuddy.
|
|
@@ -3034,6 +3113,13 @@ function makePlugin(site) {
|
|
|
3034
3113
|
// the system prompts this run WorkBuddy refused as an unapproved channel:
|
|
3035
3114
|
// their next chats go with systemAsUser at once, not refused first
|
|
3036
3115
|
const refusedSystems = new Set()
|
|
3116
|
+
// the system prompts this run WorkBuddy refused as written and took with
|
|
3117
|
+
// OPENERS' comma: their next chats go with the comma set before the first
|
|
3118
|
+
// send, as a moved one goes moved. Without it every turn of that agent —
|
|
3119
|
+
// and every step of its tool loop — pays a refused round trip first, which
|
|
3120
|
+
// is latency on each request and a pattern of channel refusals on the
|
|
3121
|
+
// account. Bounded as refusedSystems is.
|
|
3122
|
+
const commaSystems = new Set()
|
|
3037
3123
|
return async ({ client, magpie }) => {
|
|
3038
3124
|
// each account's list as last read, for FREE_ONLY: {at, list}
|
|
3039
3125
|
const lists = new Map()
|
|
@@ -3284,6 +3370,10 @@ function makePlugin(site) {
|
|
|
3284
3370
|
// words at once (see systemAsUser)
|
|
3285
3371
|
const sysKey = native ? "" : systemKeyOf(chat)
|
|
3286
3372
|
if (sysKey && refusedSystems.has(sysKey) && systemAsUserOn(chat)) changed = true
|
|
3373
|
+
// a system prompt whose comma passed once goes with the comma
|
|
3374
|
+
// set before the first send (see commaSystems), the way one
|
|
3375
|
+
// refused before goes moved above
|
|
3376
|
+
if (sysKey && commaSystems.has(sysKey) && commaPausedOn(chat)) changed = true
|
|
3287
3377
|
// shaped is a body as such a model takes it. The first try's
|
|
3288
3378
|
// is made on the chat already parsed, after the move above,
|
|
3289
3379
|
// as shaped would make it of the body written; a resend with
|
|
@@ -3311,7 +3401,27 @@ function makePlugin(site) {
|
|
|
3311
3401
|
else res = new Response(text, { status: res.status, statusText: res.statusText, headers: res.headers })
|
|
3312
3402
|
}
|
|
3313
3403
|
if (res.status === 400 && !native) { let text = await res.text()
|
|
3314
|
-
|
|
3404
|
+
// the vendor may know a shape of the agent's opening that FLAGGED
|
|
3405
|
+
// doesn't: the same words said with one more comma pass it, and
|
|
3406
|
+
// that is tried before the system prompt is moved out of its slot.
|
|
3407
|
+
// The roles the comma went into are kept: one set in the system
|
|
3408
|
+
// prompt is what its next chats go with, one set in an assistant
|
|
3409
|
+
// message alone says nothing about the next chat's.
|
|
3410
|
+
const roles = new Set()
|
|
3411
|
+
const paused = REFUSED.test(text) ? commaPaused(body, roles) : null
|
|
3412
|
+
if (paused) {
|
|
3413
|
+
body = paused
|
|
3414
|
+
res = await send(shaped(body))
|
|
3415
|
+
if (res.status === 400) text = await res.text()
|
|
3416
|
+
// not refused this time: the comma is what passed the
|
|
3417
|
+
// vendor's check, so this system prompt's next chats go
|
|
3418
|
+
// with it set before the first send
|
|
3419
|
+
else if (sysKey && roles.has("system")) {
|
|
3420
|
+
commaSystems.add(sysKey)
|
|
3421
|
+
if (commaSystems.size > 256) commaSystems.delete(commaSystems.values().next().value)
|
|
3422
|
+
}
|
|
3423
|
+
}
|
|
3424
|
+
const moved = res.status === 400 && REFUSED.test(text) && sysKey && !refusedSystems.has(sysKey) ? systemAsUser(body) : null
|
|
3315
3425
|
if (moved) {
|
|
3316
3426
|
refusedSystems.add(sysKey)
|
|
3317
3427
|
if (refusedSystems.size > 256) refusedSystems.delete(refusedSystems.values().next().value)
|
|
@@ -3440,6 +3550,13 @@ function makePlugin(site) {
|
|
|
3440
3550
|
// apart from WorkBuddy's own words.
|
|
3441
3551
|
const REFUSED_HINT =
|
|
3442
3552
|
"WorkBuddy refused words in this client's prompt (most often its system prompt); use it from Hermes, OpenCode or Pi, or add another provider to this group"
|
|
3553
|
+
|
|
3554
|
+
// REFUSED_DOC is where a refusal's fix is written down, for whoever holds the
|
|
3555
|
+
// error next: the person reading it, or the agent they paste it to. It goes
|
|
3556
|
+
// before the hint, never after, because magpie's Routing page tells the hint
|
|
3557
|
+
// from WorkBuddy's own words by the error ending in it.
|
|
3558
|
+
const REFUSED_DOC = "https://github.com/magpie-community/plugins/blob/main/packages/workbuddy/REFUSED.md"
|
|
3559
|
+
|
|
3443
3560
|
const REFUSED = /unapproved channel|illegal api invocation/i
|
|
3444
3561
|
|
|
3445
3562
|
// BAD_SIGNATURE is WorkBuddy's refusal of a thinking block whose signature it
|
|
@@ -3518,7 +3635,11 @@ async function explained(res) {
|
|
|
3518
3635
|
const hint = isRefused ? REFUSED_HINT : isRestricted ? RESTRICTED_HINT : EXHAUSTED_HINT
|
|
3519
3636
|
const type = isRefused || isRestricted ? "permission_error" : "insufficient_quota"
|
|
3520
3637
|
const errCode = isRefused ? null : isRestricted ? RESTRICTED_CODE : 14018
|
|
3521
|
-
|
|
3638
|
+
// a refusal says where its fix is written down, so whoever holds the error
|
|
3639
|
+
// — the person, or the agent they paste it to — can go and read it. It goes
|
|
3640
|
+
// before the hint, never after: the hint is the message's end.
|
|
3641
|
+
const where = isRefused ? `${REFUSED_DOC} — ` : ""
|
|
3642
|
+
msg = `${msg} — ${where}${hint}`
|
|
3522
3643
|
const headers = new Headers(res.headers)
|
|
3523
3644
|
headers.set("content-type", "application/json")
|
|
3524
3645
|
headers.delete("content-length")
|
|
@@ -3531,4 +3652,4 @@ export const WorkBuddyAuthPlugin = makePlugin(SITES.workbuddy)
|
|
|
3531
3652
|
export const WorkBuddyAIAuthPlugin = makePlugin(SITES["workbuddy-ai"])
|
|
3532
3653
|
|
|
3533
3654
|
// for tests
|
|
3534
|
-
export const _internal = { idFor, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, accountName, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, scopeOf, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, freeWindowOf, tokensShort, freeWindowRows, freeWindowLedger, readLedgerTail, pinned, rowTime, rowAccount, ofAccount, windowOffset, windowReset, FREE_WINDOW_MODELS, FREE_WINDOW_SPAN, FREE_WINDOW_GAP, FREE_WINDOW_LEAD, checkinOf, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
|
|
3655
|
+
export const _internal = { idFor, replyAfterToolsOn, withDefaultEffort, defaultEffortOf, gzipped, FREE_ONLY, PRICE_TTL, keyAccount, remoteConfig, cycleOf, typeUnions, systemAsUser, systemKey, DEFAULT_SYSTEM, EDGE_READS, fitShape, replyAfterTools, AFTER_TOOLS_REFUSED, enterpriseUsageOf, accountName, notDesktop, textOnly, runsOutOf, packagesOf, endOf, enterpriseHeaders, enterpriseOf, liveModels, browserSignIn, withSystem, asReasoning, normalizeToolChoice, fitTools, fitted, renamed, unflagged, usageOf, desktopHeld, explained, REFUSED_HINT, RESTRICTED_HINT, EXHAUSTED_HINT, REFUSED_DOC, commaPaused, commaPausedOn, opened, freeCredits, creditsOf, priceOf, fresh, SITES, renewing, renewed, SESSION, attend, scopeOf, turnKey, conversationKey, signatureOf, isMessages, withoutUnsigned, withoutSignedThinking, prepared, answeredBefore, nativeOf, modelOf, configModels, freeWindowOf, tokensShort, freeWindowRows, freeWindowLedger, readLedgerTail, pinned, rowTime, rowAccount, ofAccount, windowOffset, windowReset, FREE_WINDOW_MODELS, FREE_WINDOW_SPAN, FREE_WINDOW_GAP, FREE_WINDOW_LEAD, checkinOf, API_MESSAGES: NPM_MESSAGES, API_CHAT: NPM }
|
package/package.json
CHANGED
|
@@ -1,12 +1,24 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@magpie-community/opencode-workbuddy-auth",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "OpenCode provider plugins for WorkBuddy's plans: WorkBuddy (CodeBuddy, copilot.tencent.com) and WorkBuddy AI (www.workbuddy.ai)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./index.mjs",
|
|
7
7
|
"exports": "./index.mjs",
|
|
8
|
-
"files": [
|
|
9
|
-
|
|
8
|
+
"files": [
|
|
9
|
+
"index.mjs",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"opencode",
|
|
14
|
+
"opencode-plugin",
|
|
15
|
+
"workbuddy",
|
|
16
|
+
"codebuddy"
|
|
17
|
+
],
|
|
10
18
|
"license": "MIT",
|
|
11
|
-
"repository": {
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/magpie-community/plugins.git",
|
|
22
|
+
"directory": "packages/workbuddy"
|
|
23
|
+
}
|
|
12
24
|
}
|