@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.
Files changed (3) hide show
  1. package/README.md +27 -15
  2. package/index.mjs +126 -5
  3. 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. A refusal of words
178
- not reworded yet is sent once more, since 0.1.29, with the system prompt
179
- moved into the first user message (inside `<instructions>` tags) and
180
- WorkBuddy's default system prompt in its place: WorkBuddy checks only the
181
- system and assistant messages, and the line it matched is then in neither.
182
- A system message later in the chat becomes a user message where it stands.
183
- The plugin remembers each refused system prompt while it is loaded, so that
184
- agent's next chats go moved at once. The move can't reach an assistant
185
- message, so words the model quotes back into one keep that chat refused
186
- until they are in the list reworded in every message (since 0.1.30, the "Web GUI whose URL the runtime
187
- context names" line, #291); otherwise only a new chat clears it. A refusal
188
- that stands after the move ends in a hint that names no one client
189
- (since 0.1.28): WorkBuddy refused words in this client's prompt; use it from
190
- Hermes, OpenCode or Pi, or add another provider to the group. Neither
191
- refusal rests the account.
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
- [/You are an AI agent powered by DeepSeek Harness\.?/gi, "You are an AI agent that helps the user with programming tasks."],
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
- const moved = REFUSED.test(text) && sysKey && !refusedSystems.has(sysKey) ? systemAsUser(body) : null
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
- msg = `${msg} — ${hint}`
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.1",
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": ["index.mjs", "README.md"],
9
- "keywords": ["opencode", "opencode-plugin", "workbuddy", "codebuddy"],
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": { "type": "git", "url": "git+https://github.com/magpie-community/plugins.git", "directory": "packages/workbuddy" }
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "git+https://github.com/magpie-community/plugins.git",
22
+ "directory": "packages/workbuddy"
23
+ }
12
24
  }