dsh-dbhub-live 5.2.0-dev.3 → 5.2.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/AGENTS.md +116 -116
- package/README.en.md +22 -22
- package/README.md +23 -23
- package/doc/ARCHITECTURE.md +108 -108
- package/doc/CLIENT.md +124 -124
- package/doc/DESENSITIZE.md +56 -56
- package/doc/DEVELOPMENT.md +139 -139
- package/doc/IMPACT-MATRIX.md +36 -36
- package/doc/RELEASE.md +93 -93
- package/lib/meta.mjs +267 -267
- package/lib/policy.mjs +92 -92
- package/lib/update.mjs +244 -244
- package/package.json +1 -1
- package/scripts/desensitize-check.mjs +331 -331
- package/scripts/docs-check.mjs +214 -214
- package/scripts/release-preflight.mjs +180 -180
- package/test/meta.test.mjs +234 -234
- package/test/policy.test.mjs +132 -132
- package/test/update.test.mjs +270 -270
package/test/policy.test.mjs
CHANGED
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
// Model-facing database-access policy: the system-prompt section that keeps
|
|
2
|
-
// database work on the dbhub_* tools and out of shell-level clients.
|
|
3
|
-
//
|
|
4
|
-
// Gates asserted here:
|
|
5
|
-
// 1. the text states every clause the incident requires — the authorized
|
|
6
|
-
// route, the CLI/script ban, "an AGENTS.md recipe is not authorization",
|
|
7
|
-
// the read-only boundary, and the ask-the-user escape hatch;
|
|
8
|
-
// 2. COEXISTENCE: the authority claim is scoped to this plugin's own
|
|
9
|
-
// connections, a database this plugin does not cover is explicitly handed
|
|
10
|
-
// to the user's other plugin, and a configured read-only target may not be
|
|
11
|
-
// reached through another plugin either;
|
|
12
|
-
// 3. a DISABLED plugin, or a user who turned the announcement off,
|
|
13
|
-
// contributes nothing (an empty section is dropped), so the prompt can
|
|
14
|
-
// never claim dbhub is the only route while every call answers
|
|
15
|
-
// "插件已禁用";
|
|
16
|
-
// 4. registration targets the optional `systemPrompt` service, is owned by an
|
|
17
|
-
// effect (a reload re-runs apply, and a duplicate section name throws in
|
|
18
|
-
// one scope), and is a silent no-op where that service is absent
|
|
19
|
-
// (older dsh deployments);
|
|
20
|
-
// 5. the text stays inside its token budget and stays zero-knowledge
|
|
21
|
-
// (no DSN-looking string, no `{{variable}}` that assembly would reject).
|
|
22
|
-
|
|
23
|
-
import test from 'node:test'
|
|
24
|
-
import assert from 'node:assert/strict'
|
|
25
|
-
import { mkdtempSync, rmSync } from 'node:fs'
|
|
26
|
-
import { tmpdir } from 'node:os'
|
|
27
|
-
import { join } from 'node:path'
|
|
28
|
-
|
|
29
|
-
const home = mkdtempSync(join(tmpdir(), 'dsh-dbhub-policy-'))
|
|
30
|
-
process.env.DSH_HOME = home
|
|
31
|
-
|
|
32
|
-
// Plain specifiers only: these are the exact module instances the plugin
|
|
33
|
-
// resolves, so `setEnabled`/`applyPatch` here reach the state the policy reads.
|
|
34
|
-
const state = await import('../lib/state.mjs')
|
|
35
|
-
const options = await import('../lib/options.mjs')
|
|
36
|
-
const policy = await import('../lib/policy.mjs')
|
|
37
|
-
|
|
38
|
-
test.after(() => {
|
|
39
|
-
rmSync(home, { recursive: true, force: true })
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
test('the policy text states every clause the incident requires', () => {
|
|
43
|
-
state.setEnabled(true)
|
|
44
|
-
options.applyPatch({ announceAccessPolicy: true })
|
|
45
|
-
const text = policy.dbhubPolicyText()
|
|
46
|
-
assert.match(text, /dbhub_\* 工具/, 'the authorized route')
|
|
47
|
-
assert.match(text, /唯一授权途径/, 'the authority claim that outranks a workspace recipe')
|
|
48
|
-
assert.match(text, /psql/, 'the shell-level bypass, named')
|
|
49
|
-
assert.match(text, /AGENTS\.md/, 'workspace recipes are explicitly not authorization')
|
|
50
|
-
assert.match(text, /READONLY_VIOLATION/, 'the read-only boundary')
|
|
51
|
-
assert.match(text, /请用户授权/, 'the escape hatch: ask the user, do not improvise')
|
|
52
|
-
assert.match(text, /不要自行绕过/)
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
test('the authority claim is scoped, and other plugins keep the databases this one does not cover', () => {
|
|
56
|
-
options.applyPatch({ announceAccessPolicy: true })
|
|
57
|
-
const text = policy.dbhubPolicyText()
|
|
58
|
-
// Scoped to THIS plugin's connections — not "every database on this machine".
|
|
59
|
-
assert.match(text, /本插件已配置的连接/)
|
|
60
|
-
assert.doesNotMatch(text, /本机唯一授权途径/)
|
|
61
|
-
// A target this plugin cannot serve goes to the user's other plugin...
|
|
62
|
-
assert.match(text, /不支持或未配置的库/)
|
|
63
|
-
assert.match(text, /其它数据库插件是允许的/)
|
|
64
|
-
// ...while a configured read-only target stays closed on every route.
|
|
65
|
-
assert.match(text, /包括换用别的数据库插件/)
|
|
66
|
-
// Overlapping targets have a deterministic tie-break plus a fallback question.
|
|
67
|
-
assert.match(text, /默认用本插件的 source/)
|
|
68
|
-
assert.match(text, /先问用户/)
|
|
69
|
-
})
|
|
70
|
-
|
|
71
|
-
test('a disabled plugin, or the announcement switched off, contributes an EMPTY section', () => {
|
|
72
|
-
options.applyPatch({ announceAccessPolicy: true })
|
|
73
|
-
state.setEnabled(false)
|
|
74
|
-
assert.equal(policy.dbhubPolicyText(), '')
|
|
75
|
-
state.setEnabled(true)
|
|
76
|
-
assert.notEqual(policy.dbhubPolicyText(), '')
|
|
77
|
-
|
|
78
|
-
options.applyPatch({ announceAccessPolicy: false })
|
|
79
|
-
assert.equal(policy.dbhubPolicyText(), '', 'the user turned the announcement off')
|
|
80
|
-
options.applyPatch({ announceAccessPolicy: true })
|
|
81
|
-
assert.notEqual(policy.dbhubPolicyText(), '')
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
test('the policy text stays inside its budget and its zero-knowledge frame', () => {
|
|
85
|
-
state.setEnabled(true)
|
|
86
|
-
options.applyPatch({ announceAccessPolicy: true })
|
|
87
|
-
const text = policy.dbhubPolicyText()
|
|
88
|
-
assert.ok(text.length <= 420, 'token budget: ' + String(text.length) + ' chars')
|
|
89
|
-
// A real-looking DSN would leak a credential into the system prompt.
|
|
90
|
-
assert.doesNotMatch(text, /[a-z]+:\/\/[^/\s]*:[^@\s]+@/)
|
|
91
|
-
// `{{name}}` is interpolated strictly by prompt assembly: an unregistered
|
|
92
|
-
// name would make every assembly throw, i.e. the model would get no prompt.
|
|
93
|
-
assert.doesNotMatch(text, /\{\{[a-z_]+\}\}/)
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
test('registration injects the optional systemPrompt service and owns the section', () => {
|
|
97
|
-
const injected = []
|
|
98
|
-
const sections = []
|
|
99
|
-
const effects = []
|
|
100
|
-
const ctx = {
|
|
101
|
-
inject(deps, cb) {
|
|
102
|
-
injected.push(deps)
|
|
103
|
-
cb({
|
|
104
|
-
systemPrompt: {
|
|
105
|
-
section(def) {
|
|
106
|
-
sections.push(def)
|
|
107
|
-
return () => sections.splice(sections.indexOf(def), 1)
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
effect(fn, label) {
|
|
111
|
-
effects.push(label)
|
|
112
|
-
return fn()
|
|
113
|
-
},
|
|
114
|
-
})
|
|
115
|
-
},
|
|
116
|
-
}
|
|
117
|
-
policy.registerDbhubPolicy(ctx)
|
|
118
|
-
assert.deepEqual(injected, [['systemPrompt']])
|
|
119
|
-
assert.equal(sections.length, 1)
|
|
120
|
-
assert.equal(sections[0].name, policy.POLICY_SECTION)
|
|
121
|
-
assert.equal(sections[0].order, policy.POLICY_ORDER)
|
|
122
|
-
assert.equal(typeof sections[0].text, 'function', 'the text is a provider, not a snapshot')
|
|
123
|
-
assert.equal(sections[0].text(), policy.dbhubPolicyText())
|
|
124
|
-
// The section is effect-owned, so a re-run of apply() disposes it instead of
|
|
125
|
-
// throwing on the duplicate name.
|
|
126
|
-
assert.deepEqual(effects, ['dsh-dbhub-live: access policy'])
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
test('a deployment without systemPrompt registers nothing and does not throw', () => {
|
|
130
|
-
const never = { inject() { /* the callback never fires without the service */ } }
|
|
131
|
-
assert.doesNotThrow(() => policy.registerDbhubPolicy(never))
|
|
132
|
-
})
|
|
1
|
+
// Model-facing database-access policy: the system-prompt section that keeps
|
|
2
|
+
// database work on the dbhub_* tools and out of shell-level clients.
|
|
3
|
+
//
|
|
4
|
+
// Gates asserted here:
|
|
5
|
+
// 1. the text states every clause the incident requires — the authorized
|
|
6
|
+
// route, the CLI/script ban, "an AGENTS.md recipe is not authorization",
|
|
7
|
+
// the read-only boundary, and the ask-the-user escape hatch;
|
|
8
|
+
// 2. COEXISTENCE: the authority claim is scoped to this plugin's own
|
|
9
|
+
// connections, a database this plugin does not cover is explicitly handed
|
|
10
|
+
// to the user's other plugin, and a configured read-only target may not be
|
|
11
|
+
// reached through another plugin either;
|
|
12
|
+
// 3. a DISABLED plugin, or a user who turned the announcement off,
|
|
13
|
+
// contributes nothing (an empty section is dropped), so the prompt can
|
|
14
|
+
// never claim dbhub is the only route while every call answers
|
|
15
|
+
// "插件已禁用";
|
|
16
|
+
// 4. registration targets the optional `systemPrompt` service, is owned by an
|
|
17
|
+
// effect (a reload re-runs apply, and a duplicate section name throws in
|
|
18
|
+
// one scope), and is a silent no-op where that service is absent
|
|
19
|
+
// (older dsh deployments);
|
|
20
|
+
// 5. the text stays inside its token budget and stays zero-knowledge
|
|
21
|
+
// (no DSN-looking string, no `{{variable}}` that assembly would reject).
|
|
22
|
+
|
|
23
|
+
import test from 'node:test'
|
|
24
|
+
import assert from 'node:assert/strict'
|
|
25
|
+
import { mkdtempSync, rmSync } from 'node:fs'
|
|
26
|
+
import { tmpdir } from 'node:os'
|
|
27
|
+
import { join } from 'node:path'
|
|
28
|
+
|
|
29
|
+
const home = mkdtempSync(join(tmpdir(), 'dsh-dbhub-policy-'))
|
|
30
|
+
process.env.DSH_HOME = home
|
|
31
|
+
|
|
32
|
+
// Plain specifiers only: these are the exact module instances the plugin
|
|
33
|
+
// resolves, so `setEnabled`/`applyPatch` here reach the state the policy reads.
|
|
34
|
+
const state = await import('../lib/state.mjs')
|
|
35
|
+
const options = await import('../lib/options.mjs')
|
|
36
|
+
const policy = await import('../lib/policy.mjs')
|
|
37
|
+
|
|
38
|
+
test.after(() => {
|
|
39
|
+
rmSync(home, { recursive: true, force: true })
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('the policy text states every clause the incident requires', () => {
|
|
43
|
+
state.setEnabled(true)
|
|
44
|
+
options.applyPatch({ announceAccessPolicy: true })
|
|
45
|
+
const text = policy.dbhubPolicyText()
|
|
46
|
+
assert.match(text, /dbhub_\* 工具/, 'the authorized route')
|
|
47
|
+
assert.match(text, /唯一授权途径/, 'the authority claim that outranks a workspace recipe')
|
|
48
|
+
assert.match(text, /psql/, 'the shell-level bypass, named')
|
|
49
|
+
assert.match(text, /AGENTS\.md/, 'workspace recipes are explicitly not authorization')
|
|
50
|
+
assert.match(text, /READONLY_VIOLATION/, 'the read-only boundary')
|
|
51
|
+
assert.match(text, /请用户授权/, 'the escape hatch: ask the user, do not improvise')
|
|
52
|
+
assert.match(text, /不要自行绕过/)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('the authority claim is scoped, and other plugins keep the databases this one does not cover', () => {
|
|
56
|
+
options.applyPatch({ announceAccessPolicy: true })
|
|
57
|
+
const text = policy.dbhubPolicyText()
|
|
58
|
+
// Scoped to THIS plugin's connections — not "every database on this machine".
|
|
59
|
+
assert.match(text, /本插件已配置的连接/)
|
|
60
|
+
assert.doesNotMatch(text, /本机唯一授权途径/)
|
|
61
|
+
// A target this plugin cannot serve goes to the user's other plugin...
|
|
62
|
+
assert.match(text, /不支持或未配置的库/)
|
|
63
|
+
assert.match(text, /其它数据库插件是允许的/)
|
|
64
|
+
// ...while a configured read-only target stays closed on every route.
|
|
65
|
+
assert.match(text, /包括换用别的数据库插件/)
|
|
66
|
+
// Overlapping targets have a deterministic tie-break plus a fallback question.
|
|
67
|
+
assert.match(text, /默认用本插件的 source/)
|
|
68
|
+
assert.match(text, /先问用户/)
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
test('a disabled plugin, or the announcement switched off, contributes an EMPTY section', () => {
|
|
72
|
+
options.applyPatch({ announceAccessPolicy: true })
|
|
73
|
+
state.setEnabled(false)
|
|
74
|
+
assert.equal(policy.dbhubPolicyText(), '')
|
|
75
|
+
state.setEnabled(true)
|
|
76
|
+
assert.notEqual(policy.dbhubPolicyText(), '')
|
|
77
|
+
|
|
78
|
+
options.applyPatch({ announceAccessPolicy: false })
|
|
79
|
+
assert.equal(policy.dbhubPolicyText(), '', 'the user turned the announcement off')
|
|
80
|
+
options.applyPatch({ announceAccessPolicy: true })
|
|
81
|
+
assert.notEqual(policy.dbhubPolicyText(), '')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('the policy text stays inside its budget and its zero-knowledge frame', () => {
|
|
85
|
+
state.setEnabled(true)
|
|
86
|
+
options.applyPatch({ announceAccessPolicy: true })
|
|
87
|
+
const text = policy.dbhubPolicyText()
|
|
88
|
+
assert.ok(text.length <= 420, 'token budget: ' + String(text.length) + ' chars')
|
|
89
|
+
// A real-looking DSN would leak a credential into the system prompt.
|
|
90
|
+
assert.doesNotMatch(text, /[a-z]+:\/\/[^/\s]*:[^@\s]+@/)
|
|
91
|
+
// `{{name}}` is interpolated strictly by prompt assembly: an unregistered
|
|
92
|
+
// name would make every assembly throw, i.e. the model would get no prompt.
|
|
93
|
+
assert.doesNotMatch(text, /\{\{[a-z_]+\}\}/)
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
test('registration injects the optional systemPrompt service and owns the section', () => {
|
|
97
|
+
const injected = []
|
|
98
|
+
const sections = []
|
|
99
|
+
const effects = []
|
|
100
|
+
const ctx = {
|
|
101
|
+
inject(deps, cb) {
|
|
102
|
+
injected.push(deps)
|
|
103
|
+
cb({
|
|
104
|
+
systemPrompt: {
|
|
105
|
+
section(def) {
|
|
106
|
+
sections.push(def)
|
|
107
|
+
return () => sections.splice(sections.indexOf(def), 1)
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
effect(fn, label) {
|
|
111
|
+
effects.push(label)
|
|
112
|
+
return fn()
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
},
|
|
116
|
+
}
|
|
117
|
+
policy.registerDbhubPolicy(ctx)
|
|
118
|
+
assert.deepEqual(injected, [['systemPrompt']])
|
|
119
|
+
assert.equal(sections.length, 1)
|
|
120
|
+
assert.equal(sections[0].name, policy.POLICY_SECTION)
|
|
121
|
+
assert.equal(sections[0].order, policy.POLICY_ORDER)
|
|
122
|
+
assert.equal(typeof sections[0].text, 'function', 'the text is a provider, not a snapshot')
|
|
123
|
+
assert.equal(sections[0].text(), policy.dbhubPolicyText())
|
|
124
|
+
// The section is effect-owned, so a re-run of apply() disposes it instead of
|
|
125
|
+
// throwing on the duplicate name.
|
|
126
|
+
assert.deepEqual(effects, ['dsh-dbhub-live: access policy'])
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
test('a deployment without systemPrompt registers nothing and does not throw', () => {
|
|
130
|
+
const never = { inject() { /* the callback never fires without the service */ } }
|
|
131
|
+
assert.doesNotThrow(() => policy.registerDbhubPolicy(never))
|
|
132
|
+
})
|