dsh-prompt-picker 0.7.0 → 0.8.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 +19 -0
- package/README.md +1 -0
- package/README.zh.md +1 -0
- package/index.js +67 -14
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.0
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
- **Same-origin guard on the RPC route.** A webpage could make a browser POST
|
|
8
|
+
`text/plain` JSON at the loopback port (no preflight) and silently add or
|
|
9
|
+
check prompts. `/_dsh/prompt-picker/rpc` now refuses a request before
|
|
10
|
+
reading anything unless Origin (when present) equals the server's own
|
|
11
|
+
origin, Host is loopback or the server's own hostname, and POST bodies are
|
|
12
|
+
`application/json`; refusals answer `403 {code:'forbidden'}` and change
|
|
13
|
+
nothing.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- **Slim debug log by default.** The per-assembly line keeps `t`, `sub`,
|
|
18
|
+
`why`, `injected`; the context dump (`keys` + `ctx`) is only written with
|
|
19
|
+
`debugContext: true` in the row config. Fixes the stale
|
|
20
|
+
`full-throttle-debug.log` filename in the header comment.
|
|
21
|
+
|
|
3
22
|
## 0.7.0
|
|
4
23
|
|
|
5
24
|
### Added
|
package/README.md
CHANGED
|
@@ -14,6 +14,7 @@ One bolt icon ⚡ in the composer toolbar: click it and check the system prompts
|
|
|
14
14
|
- **Per-row scope**: all / main agent only / sub-agents only (click the chip at the end of a row; teammate sessions count as sub-agents)
|
|
15
15
|
- **Manage**: add, edit (the built-in "Full Throttle" row included), delete one by one, or clear every saved row at once
|
|
16
16
|
- **Survives restarts**: state lives in `~/.dsh/prompt-picker.json`
|
|
17
|
+
- **Local debug log**: each injection appends one slim JSON line (scope verdict + names) to `~/.dsh/prompt-picker-debug.log`; add `debugContext: true` to the row config for the full context dump. Local only, auto-resets past 256 KiB
|
|
17
18
|
|
|
18
19
|
## Install
|
|
19
20
|
|
package/README.zh.md
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
- **每条可设作用域**:全部 / 仅主 agent / 仅子 agent(点行尾圆片切换;teammate 会话也算子 agent)
|
|
15
15
|
- **管理**:新增、修改(内置「火力全开」行也能改)、单条删除、一键全清自建条目
|
|
16
16
|
- **重启不丢**:状态保存在 `~/.dsh/prompt-picker.json`
|
|
17
|
+
- **本地调试日志**:每次注入在 `~/.dsh/prompt-picker-debug.log` 记一行极简 JSON(作用域判定与注入名);需要完整上下文转储时在配置里加 `debugContext: true`。纯本地,超 256KiB 自动清零
|
|
17
18
|
|
|
18
19
|
## 安装
|
|
19
20
|
|
package/index.js
CHANGED
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
* built-in row is generated from config, can be edited like any row — the
|
|
10
10
|
* edit persists as a `builtin` override in the state file and disappears
|
|
11
11
|
* again once the row is back to its generated default — but cannot be
|
|
12
|
-
* deleted. One dynamic system-prompt section receives
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* deleted. One dynamic system-prompt section receives the assembly context;
|
|
13
|
+
* rows whose scope excludes the assembling agent (main agent vs sub-agent)
|
|
14
|
+
* are filtered out. Every assembly appends one slim JSON line (sub, why,
|
|
15
|
+
* injected names) to `$DSH_HOME/prompt-picker-debug.log`; the full context
|
|
16
|
+
* dump (keys + ctx) is only written with `debugContext: true`. The RPC
|
|
17
|
+
* route admits only same-origin callers: matching Origin (when present),
|
|
18
|
+
* loopback-or-own Host, and JSON Content-Type on POST.
|
|
17
19
|
*/
|
|
18
20
|
import { appendFile, mkdir, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises'
|
|
19
21
|
import { appendFileSync, statSync, writeFileSync } from 'node:fs'
|
|
@@ -52,6 +54,15 @@ function messageOf(error) {
|
|
|
52
54
|
return error instanceof Error ? error.message : String(error)
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
/** Hostname part of an absolute origin URL, lowercase ('' when unparseable). */
|
|
58
|
+
function hostnameOf(url) {
|
|
59
|
+
try {
|
|
60
|
+
return new URL(url).hostname.toLowerCase()
|
|
61
|
+
} catch {
|
|
62
|
+
return ''
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
55
66
|
function scopeOf(value) {
|
|
56
67
|
return SCOPES.includes(value) ? value : 'both'
|
|
57
68
|
}
|
|
@@ -158,6 +169,8 @@ class FullThrottleService {
|
|
|
158
169
|
this.home = resolveDshHome(config?.dshHome)
|
|
159
170
|
this.file = join(this.home, STATE_FILE)
|
|
160
171
|
this.debugFile = join(this.home, DEBUG_FILE)
|
|
172
|
+
/** Full context dump in the debug log; slim line otherwise. */
|
|
173
|
+
this.debugContext = config?.debugContext === true
|
|
161
174
|
this.builtin = { id: BUILTIN_ID, name: '火力全开', text: instructionsOf(config), scope: 'both' }
|
|
162
175
|
/** The freshly generated built-in row; a persisted override is measured against it. */
|
|
163
176
|
this.builtinDefault = { name: this.builtin.name, text: this.builtin.text, scope: this.builtin.scope }
|
|
@@ -398,21 +411,27 @@ class FullThrottleService {
|
|
|
398
411
|
}
|
|
399
412
|
}
|
|
400
413
|
|
|
401
|
-
/**
|
|
414
|
+
/**
|
|
415
|
+
* One JSON line per assembly (sub, why, injected names); the compact
|
|
416
|
+
* context dump (keys + ctx) only with `debugContext: true`. The file
|
|
417
|
+
* restarts past 256 KiB.
|
|
418
|
+
*/
|
|
402
419
|
logAssembly(context, decision, injected) {
|
|
403
420
|
try {
|
|
404
421
|
try {
|
|
405
422
|
if (statSync(this.debugFile).size > 256 * 1024) writeFileSync(this.debugFile, '')
|
|
406
423
|
} catch {}
|
|
407
|
-
const line =
|
|
424
|
+
const line = {
|
|
408
425
|
t: new Date().toISOString(),
|
|
409
426
|
sub: decision.sub,
|
|
410
427
|
why: decision.why,
|
|
411
428
|
injected,
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
429
|
+
}
|
|
430
|
+
if (this.debugContext) {
|
|
431
|
+
line.keys = context !== null && typeof context === 'object' ? Object.keys(context).slice(0, 24) : []
|
|
432
|
+
line.ctx = debugRepr(context)
|
|
433
|
+
}
|
|
434
|
+
appendFileSync(this.debugFile, JSON.stringify(line) + '\n', 'utf8')
|
|
416
435
|
} catch {}
|
|
417
436
|
}
|
|
418
437
|
|
|
@@ -442,10 +461,39 @@ async function readBody(req, maxBytes = 256 * 1024) {
|
|
|
442
461
|
return Buffer.concat(chunks).toString('utf8')
|
|
443
462
|
}
|
|
444
463
|
|
|
445
|
-
/**
|
|
464
|
+
/** Hostnames a loopback-served RPC also admits. */
|
|
465
|
+
const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1'])
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Same-origin RPC the browser half calls: GET = state, POST = toggle|clear|add|setScope|update|delete|deleteAll.
|
|
469
|
+
* Cross-site callers are rejected before anything is read: a browser can be
|
|
470
|
+
* made to POST `text/plain` JSON at this port from any webpage, so Origin
|
|
471
|
+
* (when present) must equal the server's own origin, Host must be loopback
|
|
472
|
+
* or the server's own hostname, and POST bodies must be `application/json`.
|
|
473
|
+
*/
|
|
446
474
|
class RpcBackend {
|
|
447
|
-
constructor(service) {
|
|
475
|
+
constructor(service, originOf) {
|
|
448
476
|
this.service = service
|
|
477
|
+
this.originOf = originOf
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/** '' when the request may pass; otherwise the reason it was refused. */
|
|
481
|
+
admit(req) {
|
|
482
|
+
const headers = req.headers ?? {}
|
|
483
|
+
const expected = this.originOf()
|
|
484
|
+
const origin = typeof headers.origin === 'string' ? headers.origin : ''
|
|
485
|
+
if (origin !== '' && origin !== expected) return 'origin ' + origin
|
|
486
|
+
const host = typeof headers.host === 'string' ? headers.host : ''
|
|
487
|
+
if (host !== '') {
|
|
488
|
+
const hostname = host.startsWith('[') ? host.slice(1, host.indexOf(']')) : host.split(':')[0]
|
|
489
|
+
const ownHostname = hostnameOf(expected)
|
|
490
|
+
if (hostname.toLowerCase() !== ownHostname && !LOOPBACK_HOSTS.has(hostname.toLowerCase())) return 'host ' + host
|
|
491
|
+
}
|
|
492
|
+
if (req.method === 'POST') {
|
|
493
|
+
const type = String(headers['content-type'] ?? '').toLowerCase()
|
|
494
|
+
if (!type.startsWith('application/json')) return 'content-type ' + (type === '' ? '(none)' : type)
|
|
495
|
+
}
|
|
496
|
+
return ''
|
|
449
497
|
}
|
|
450
498
|
|
|
451
499
|
responseJson(res, status, body) {
|
|
@@ -458,6 +506,11 @@ class RpcBackend {
|
|
|
458
506
|
}
|
|
459
507
|
|
|
460
508
|
async handle(req, res) {
|
|
509
|
+
const denied = this.admit(req)
|
|
510
|
+
if (denied !== '') {
|
|
511
|
+
this.responseJson(res, 403, { ok: false, error: { code: 'forbidden', message: 'cross-site request rejected: ' + denied } })
|
|
512
|
+
return
|
|
513
|
+
}
|
|
461
514
|
if (req.method === 'GET') {
|
|
462
515
|
try {
|
|
463
516
|
this.responseJson(res, 200, { ok: true, value: await this.service.getState() })
|
|
@@ -521,7 +574,7 @@ export function apply(ctx, config) {
|
|
|
521
574
|
'prompt-picker.prompt-section',
|
|
522
575
|
)
|
|
523
576
|
ctx.inject(['webServer'], (webCtx) => {
|
|
524
|
-
const rpc = new RpcBackend(service)
|
|
577
|
+
const rpc = new RpcBackend(service, () => webCtx.webServer.getOrigin())
|
|
525
578
|
webCtx.effect(() => {
|
|
526
579
|
const off = webCtx.webServer.register({
|
|
527
580
|
kind: 'exact',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-prompt-picker",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Prompt picker for DeepSeek Harness: multi-select system-prompt injections with per-row scopes (both/main/sub), a two-level add/edit footer menu, in-place edit of every row including the built-in one, single-row delete and clear-all, one bolt icon in the composer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|