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.
@@ -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
+ })