@stackstackstack/dsh-permission-presets 0.1.5
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/LICENSE +21 -0
- package/README.i18n.yaml +6 -0
- package/README.md +28 -0
- package/README.zh.md +28 -0
- package/lib/index.js +311 -0
- package/lib/invariant.js +28 -0
- package/lib/types/client.d.ts +10 -0
- package/lib/types/client.js +10 -0
- package/lib/types/index.d.ts +174 -0
- package/lib/types/index.js +331 -0
- package/lib/types/invariant.d.ts +13 -0
- package/lib/types/invariant.js +32 -0
- package/lib/types/types.d.ts +42 -0
- package/lib/types/types.js +12 -0
- package/package.json +71 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 DeepSeek
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.i18n.yaml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
|
|
2
|
+
# side as of the last confirmed-consistent state. Both languages carry equal authority;
|
|
3
|
+
# after editing either side, bring the other along and re-record with:
|
|
4
|
+
# pnpm run verify-translation-pairing --write packages/interaction/permission-presets/README.md
|
|
5
|
+
README.md: 7bd2b285693f1fc0c3acb8d0c4cb0e2e890feba3
|
|
6
|
+
README.zh.md: b1aeeeaf14d80e79fda9a9e80c34b1445edb2fff
|
package/README.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @stackstackstack/dsh-permission-presets
|
|
2
|
+
|
|
3
|
+
English | [中文](README.zh.md)
|
|
4
|
+
|
|
5
|
+
User-facing permission presets through `ctx.permissionPresets` ([`PermissionPresetService`](src/index.ts)). Each configured name bundles `sandbox/mode` with `approval/policy`; the defaults are `workspace-write` (`workspace-write` + `ask`) and `danger-full-access` (`danger-full-access` + `never`). UI adapters may expose the table as one selector, while sandbox execution and approval continue to consume their own knobs.
|
|
6
|
+
|
|
7
|
+
`set(session, name)` records a changed selection in a log-only `permissionPresets/preset` event, then calls each knob's setter only when its effective value changes. The selection event precedes the knob events and preserves user intent when presets share a bundle; a net-zero selection appends nothing. `current(events)` prefers a still-matching recorded selection, then the first matching table entry, and otherwise returns `custom`. Clients may display `custom` as the current value, but cannot select it.
|
|
8
|
+
|
|
9
|
+
The service owns the `permissionPresets` Settings namespace. Its `defaultPreset` is the default for future sessions: the composition entry uses `Config.defaultPreset`, or infers the preset matching the composed sandbox and approval defaults when omitted. A committed Settings change is read when the next session is created; creation pins `permissionPresets/preset`, `sandbox/mode`, and `approval/policy` into that session, so later changes never alter an existing session. A resumed seed, including an explicitly empty one marked by `session/end-seed`, preserves its effective permission and receives only missing durable facts rather than the latest user default. Mounting the service also sweeps already-live sessions, so an HMR replacement pins any session created while the plugin was absent.
|
|
10
|
+
|
|
11
|
+
The service requires a confining `ctx.shell` executor and `ctx.approval`. A table entry named `custom` throws at load. When composition defaults match no preset, the plugin requires an explicit `defaultPreset`; an independently constructed zero-event session may still derive `custom`. See the [sandbox switching design](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md).
|
|
12
|
+
|
|
13
|
+
Two optional children ship the product surfaces over the same service: a `permissions` session-projection unit (`src/types.ts` declares the key; the unit folds the three whole-value knob events and views the select — table options plus a current-only `custom` — over the composition defaults) and the `/permissionPresets` command (bare invocation reports the current preset and the table; a preset argument switches through `set`). Each child activates only when its registry (`ctx.sessionProjections` / `ctx.commands`) is composed.
|
|
14
|
+
|
|
15
|
+
## Model Experience
|
|
16
|
+
|
|
17
|
+
Indirectly, through `dsh-user-approval` and `dsh-tool-bash`, which render the approval-policy prompt, switch notice, and sandboxed tool outcomes selected by this service's knob events; `permissionPresets/preset` itself is log-only.
|
|
18
|
+
|
|
19
|
+
#### KV Cache effect
|
|
20
|
+
|
|
21
|
+
No direct invalidation; the named consumer owns any request-prefix changes.
|
|
22
|
+
|
|
23
|
+
## Known Limitations and Deferred Work
|
|
24
|
+
|
|
25
|
+
- **Only two mechanism knobs are bundled** — presets select sandbox mode and approval policy; an agent/profile choice is not part of `PresetSpec` yet.
|
|
26
|
+
- **`custom` is derived-only** — callers can switch away from an unmatched knob combination but cannot target or persist a named custom preset through this service.
|
|
27
|
+
- **The preset table is process-level** — configuration is fixed for the plugin lifetime; changing available presets requires reloading the plugin.
|
|
28
|
+
- **Stored defaults must remain in the preset table** — removing the referenced preset makes Permission settings registration fail until the `permissionPresets` section in `settings.yaml` is updated or reset.
|
package/README.zh.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# @stackstackstack/dsh-permission-presets
|
|
2
|
+
|
|
3
|
+
[English](README.md) | 中文
|
|
4
|
+
|
|
5
|
+
通过 `ctx.permissionPresets`([`PermissionPresetService`](src/index.ts))提供面向用户的权限预设。每个配置名称都会将 `sandbox/mode` 与 `approval/policy` 组成一组;默认项为 `workspace-write`(`workspace-write` + `ask`)和 `danger-full-access`(`danger-full-access` + `never`)。UI 适配器可以将该表作为单个选择器公开,而沙箱执行与审批仍分别消费各自的调节项。
|
|
6
|
+
|
|
7
|
+
`set(session, name)` 会先在仅写日志的 `permissionPresets/preset` 事件中记录已变更的选择,再仅对实际值发生变化的调节项调用 setter。选择事件先于调节项事件,并在多个预设共享同一组取值时保留用户意图;净变化为零的选择不会追加任何内容。`current(events)` 优先返回仍与当前调节项匹配的已记录选择,其次返回表中第一个匹配项,否则返回 `custom`。客户端可以把 `custom` 显示为当前值,但不能选择它。
|
|
8
|
+
|
|
9
|
+
该服务拥有 `permissionPresets` Settings namespace。其 `defaultPreset` 是未来会话的默认值:组合项使用 `Config.defaultPreset`;省略时,则推断与组合后的沙箱和审批默认值匹配的 preset。已提交的 Settings 变更会在下一个会话创建时读取;创建过程将 `permissionPresets/preset`、`sandbox/mode` 和 `approval/policy` 固定到该会话中,因此后续变更绝不会改变现有会话。恢复的 seed,包括由 `session/end-seed` 标记的显式空 seed,都会保留其有效权限,只补齐缺失的持久事实,而不会采用最新的用户默认值。挂载服务时还会遍历所有已存活会话,因此 HMR(热模块替换)会固定插件缺席期间创建的所有会话。
|
|
10
|
+
|
|
11
|
+
该服务要求存在具有约束能力的 `ctx.shell` 执行器和 `ctx.approval`。表中名为 `custom` 的条目会在加载时抛出异常。当组合默认值与任何 preset 都不匹配时,插件要求显式配置 `defaultPreset`;独立构造的零事件会话仍可能推导出 `custom`。详见[沙箱切换设计](../../../.agents/notes/implemented/feature/2026-07-06-sandbox.md)。
|
|
12
|
+
|
|
13
|
+
两个可选子功能在同一服务之上提供产品界面:`permissions` 会话投影单元(`src/types.ts` 声明该 key;单元以组合默认值为基础折叠三个全量值可调参数事件,并生成选择器视图,其中包含表内选项和仅作当前值的 `custom`)与 `/permissionPresets` 命令(不带参数调用时报告当前预设与表;预设参数经 `set` 切换)。每个子功能仅在其注册表(`ctx.sessionProjections` / `ctx.commands`)被组合时激活。
|
|
14
|
+
|
|
15
|
+
## 模型体验
|
|
16
|
+
|
|
17
|
+
间接地,通过 `dsh-user-approval` 和 `dsh-tool-bash`:二者会渲染由此服务的可调参数事件所选择的审批策略提示词、切换通知和沙箱工具结果;`permissionPresets/preset` 本身只写入日志。
|
|
18
|
+
|
|
19
|
+
#### KV Cache 影响
|
|
20
|
+
|
|
21
|
+
不会直接使缓存失效;具名消费方拥有所有请求前缀变更。
|
|
22
|
+
|
|
23
|
+
## 已知限制与暂缓事项
|
|
24
|
+
|
|
25
|
+
- **只组合两个机制级可调参数**:预设选择沙箱模式和审批策略;agent(智能体)/profile 选择尚未纳入 `PresetSpec`。
|
|
26
|
+
- **`custom` 只能推导得出**:调用方可以从不匹配的调节项组合切换出去,但无法通过此服务选中或持久化一个名为 custom 的预设。
|
|
27
|
+
- **预设表是进程级配置**:配置在插件生命周期内固定;更改可用预设必须重新加载插件。
|
|
28
|
+
- **已存储的默认值必须保留在 preset 表中**:移除被引用的 preset 会导致权限设置注册失败,直到更新或重置 `settings.yaml` 中的 `permissionPresets` 分节。
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
2
|
+
import z from "@deepseek-ai/schemastery";
|
|
3
|
+
import { z as z$1 } from "zod";
|
|
4
|
+
import { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from "@stackstackstack/dsh-sandbox-policy";
|
|
5
|
+
import { APPROVAL_POLICIES, effectiveApprovalPolicy, setApprovalPolicy } from "@stackstackstack/dsh-user-approval";
|
|
6
|
+
import { installSettingsSection, settingsNamespace } from "@stackstackstack/dsh-settings";
|
|
7
|
+
//#region lib/types/index.js
|
|
8
|
+
/**
|
|
9
|
+
* User-facing permission presets over the independent sandbox-mode and
|
|
10
|
+
* approval-policy knobs. A switch records the selected preset, then writes
|
|
11
|
+
* changed knobs through their canonical setters. Execution, prompt narration,
|
|
12
|
+
* and replay keep reading their knob folds. The preset event preserves user
|
|
13
|
+
* intent when two presets share a bundle. The read side ships as the
|
|
14
|
+
* `permissions` session projection; the write side ships as the
|
|
15
|
+
* `/permission` command — both optional children over the same service.
|
|
16
|
+
*
|
|
17
|
+
* @module dsh-permission-presets
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Returned when effective knob values match no table entry. Clients may show
|
|
21
|
+
* it as the current value, but it is never a switch target or event payload.
|
|
22
|
+
*/
|
|
23
|
+
const CUSTOM_PRESET = "custom";
|
|
24
|
+
/** Settings namespace carrying the default for future sessions. */
|
|
25
|
+
const PERMISSION_SETTINGS_NAMESPACE = settingsNamespace("permission");
|
|
26
|
+
/**
|
|
27
|
+
* Fold the last selected preset from the durable log; replay needs no catch-up
|
|
28
|
+
* state.
|
|
29
|
+
* @param events - session events in log order; other event types are ignored.
|
|
30
|
+
* @returns the last selected preset, or undefined when none was recorded.
|
|
31
|
+
*/
|
|
32
|
+
function effectivePermissionPreset(events) {
|
|
33
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
34
|
+
const event = events[index];
|
|
35
|
+
if (event.type === "permission/preset") return event.data.preset;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** State for the empty log: every knob at its composition default. */
|
|
39
|
+
const EMPTY_KNOBS = {
|
|
40
|
+
preset: null,
|
|
41
|
+
sandbox: null,
|
|
42
|
+
approval: null
|
|
43
|
+
};
|
|
44
|
+
/**
|
|
45
|
+
* One-event knob transition (the projection unit's `apply`). Uninterested
|
|
46
|
+
* events return the same reference — the registry's change gate.
|
|
47
|
+
* @param state - the folded knob state before `event`.
|
|
48
|
+
* @param event - one committed session event.
|
|
49
|
+
* @returns the next state; the same reference when the event is not a knob.
|
|
50
|
+
*/
|
|
51
|
+
function applyKnobEvent(state, event) {
|
|
52
|
+
switch (event.type) {
|
|
53
|
+
case "permission/preset": return {
|
|
54
|
+
...state,
|
|
55
|
+
preset: event.data.preset
|
|
56
|
+
};
|
|
57
|
+
case "sandbox/mode": return {
|
|
58
|
+
...state,
|
|
59
|
+
sandbox: event.data.mode
|
|
60
|
+
};
|
|
61
|
+
case "approval/policy": return {
|
|
62
|
+
...state,
|
|
63
|
+
approval: event.data.policy
|
|
64
|
+
};
|
|
65
|
+
default: return state;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
/** Whole-log knob fold (the cold-read parallel of {@link applyKnobEvent}). */
|
|
69
|
+
function foldKnobs(events) {
|
|
70
|
+
let state = EMPTY_KNOBS;
|
|
71
|
+
for (const event of events) state = applyKnobEvent(state, event);
|
|
72
|
+
return state;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Owns the deployment's permission presets and their write path. Requires a
|
|
76
|
+
* confining `ctx.shell` executor and `ctx.approval`; unmatched knob values are
|
|
77
|
+
* reported as {@link CUSTOM_PRESET}, not an error.
|
|
78
|
+
*/
|
|
79
|
+
var PermissionPresetService = class extends Service {
|
|
80
|
+
static Config = z.object({
|
|
81
|
+
presets: z.dict(z.object({
|
|
82
|
+
sandbox: z.union(SANDBOX_MODES).required(),
|
|
83
|
+
approval: z.union(APPROVAL_POLICIES).required(),
|
|
84
|
+
name: z.string(),
|
|
85
|
+
description: z.string()
|
|
86
|
+
})).default({
|
|
87
|
+
"workspace-write": {
|
|
88
|
+
sandbox: "workspace-write",
|
|
89
|
+
approval: "ask",
|
|
90
|
+
name: "workspace-write",
|
|
91
|
+
description: "Write inside the workspace and permitted temporary directories; wider retries require approval."
|
|
92
|
+
},
|
|
93
|
+
"danger-full-access": {
|
|
94
|
+
sandbox: "danger-full-access",
|
|
95
|
+
approval: "never",
|
|
96
|
+
name: "danger-full-access",
|
|
97
|
+
description: "Full file access without approval prompts."
|
|
98
|
+
}
|
|
99
|
+
}),
|
|
100
|
+
defaultPreset: z.string()
|
|
101
|
+
});
|
|
102
|
+
static inject = [
|
|
103
|
+
"shell",
|
|
104
|
+
"approval",
|
|
105
|
+
"sessions"
|
|
106
|
+
];
|
|
107
|
+
presets;
|
|
108
|
+
defaultSettings;
|
|
109
|
+
constructor(ctx, config) {
|
|
110
|
+
super(ctx, "permissionPresets");
|
|
111
|
+
this.presets = config.presets;
|
|
112
|
+
if ("custom" in this.presets) throw new Error(`permission: "${CUSTOM_PRESET}" is reserved for the derived not-a-preset state and cannot name a table entry`);
|
|
113
|
+
if (ctx.shell.sandboxMode === void 0) throw new Error("permission: the mounted bash executor does not confine (no sandboxMode) — presets bundle a sandbox mode, so composing this plugin over an unconfined executor is a misconfiguration");
|
|
114
|
+
const inferredDefault = this.derive(EMPTY_KNOBS);
|
|
115
|
+
const defaultPreset = config.defaultPreset ?? inferredDefault;
|
|
116
|
+
if (defaultPreset === "custom") throw new Error("permission: composed sandbox and approval defaults match no preset; configure defaultPreset explicitly");
|
|
117
|
+
this.resolve(defaultPreset);
|
|
118
|
+
const baseSettings = { defaultPreset };
|
|
119
|
+
this.defaultSettings = () => baseSettings;
|
|
120
|
+
const presetChoices = this.names.map((name) => {
|
|
121
|
+
const choice = z.const(name);
|
|
122
|
+
const label = this.presets[name]?.name;
|
|
123
|
+
return label === void 0 ? choice : choice.description(label);
|
|
124
|
+
});
|
|
125
|
+
installSettingsSection(ctx, PERMISSION_SETTINGS_NAMESPACE, z.object({ defaultPreset: z.union(presetChoices).required() }), baseSettings, {
|
|
126
|
+
setSource: (current) => {
|
|
127
|
+
this.defaultSettings = current;
|
|
128
|
+
},
|
|
129
|
+
onChange: () => {}
|
|
130
|
+
});
|
|
131
|
+
ctx.on("session/created", (session) => {
|
|
132
|
+
this.pinInitialPermission(session);
|
|
133
|
+
});
|
|
134
|
+
for (const session of ctx.sessions.list()) this.pinInitialPermission(session);
|
|
135
|
+
const selectSchema = z$1.object({
|
|
136
|
+
options: z$1.array(z$1.object({
|
|
137
|
+
value: z$1.string().min(1),
|
|
138
|
+
name: z$1.string().min(1),
|
|
139
|
+
description: z$1.string().optional()
|
|
140
|
+
})),
|
|
141
|
+
currentValue: z$1.string().min(1)
|
|
142
|
+
});
|
|
143
|
+
ctx.inject(["sessionProjections"], (projectionCtx) => {
|
|
144
|
+
projectionCtx.sessionProjections.register({
|
|
145
|
+
key: "permissions",
|
|
146
|
+
schema: selectSchema,
|
|
147
|
+
init: () => EMPTY_KNOBS,
|
|
148
|
+
apply: applyKnobEvent,
|
|
149
|
+
view: (state) => this.selectFor(state),
|
|
150
|
+
stateVersion: 1
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
ctx.inject(["commands"], (commandCtx) => {
|
|
154
|
+
commandCtx.commands.register({
|
|
155
|
+
name: "permission",
|
|
156
|
+
description: "Switch the permission preset (sandbox mode + approval policy)",
|
|
157
|
+
input: { hint: "<preset>" },
|
|
158
|
+
handler: ({ agent, rawInput }) => {
|
|
159
|
+
const name = rawInput.trim();
|
|
160
|
+
if (name === "") return {
|
|
161
|
+
kind: "success",
|
|
162
|
+
text: `current preset ${this.current(agent.session.events)} (available: ${this.names.join(", ")})`
|
|
163
|
+
};
|
|
164
|
+
if (!this.names.includes(name)) return {
|
|
165
|
+
kind: "error",
|
|
166
|
+
text: `unknown preset "${name}" (available: ${this.names.join(", ")})`
|
|
167
|
+
};
|
|
168
|
+
this.apply(agent.session, name, (policy) => {
|
|
169
|
+
this.ctx.approval.setPolicy(agent, policy);
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
kind: "success",
|
|
173
|
+
text: `preset ${name}`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* The advertised preset names, in the preset table's declaration order.
|
|
181
|
+
* @returns every switchable preset name.
|
|
182
|
+
*/
|
|
183
|
+
get names() {
|
|
184
|
+
return Object.keys(this.presets);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The preset currently selected as the default for future sessions.
|
|
188
|
+
* @returns the resolved settings value, or the composition default without
|
|
189
|
+
* a mounted settings provider.
|
|
190
|
+
*/
|
|
191
|
+
get defaultPreset() {
|
|
192
|
+
return this.defaultSettings().defaultPreset;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Resolve the preset matching the effective knob values. A still-matching
|
|
196
|
+
* last selection wins shared-bundle ties; otherwise the first table match
|
|
197
|
+
* wins, or {@link CUSTOM_PRESET} when no entry matches.
|
|
198
|
+
* @param events - the session's events in log order.
|
|
199
|
+
* @returns the effective preset name, or `custom` when nothing matches.
|
|
200
|
+
*/
|
|
201
|
+
current(events) {
|
|
202
|
+
return this.derive(foldKnobs(events));
|
|
203
|
+
}
|
|
204
|
+
/** Resolve the preset for one folded knob state (the shared mathematics of `current` and the projection unit). */
|
|
205
|
+
derive(state) {
|
|
206
|
+
const sandbox = state.sandbox ?? this.ctx.shell.sandboxMode;
|
|
207
|
+
const approval = state.approval ?? this.ctx.approval.config.policy ?? "ask";
|
|
208
|
+
const matches = (spec) => spec.sandbox === sandbox && spec.approval === approval;
|
|
209
|
+
if (state.preset !== null) {
|
|
210
|
+
const spec = this.presets[state.preset];
|
|
211
|
+
if (spec !== void 0 && matches(spec)) return state.preset;
|
|
212
|
+
}
|
|
213
|
+
for (const [name, spec] of Object.entries(this.presets)) if (matches(spec)) return name;
|
|
214
|
+
return CUSTOM_PRESET;
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Build the whole select value for one folded knob state: every table
|
|
218
|
+
* option in declaration order, `custom` appended exactly while derived.
|
|
219
|
+
* @param state - the folded knob overrides.
|
|
220
|
+
* @returns the `permissions` projection payload.
|
|
221
|
+
*/
|
|
222
|
+
selectFor(state) {
|
|
223
|
+
const currentValue = this.derive(state);
|
|
224
|
+
return {
|
|
225
|
+
options: [...this.names.map((name) => this.optionOf(name)), ...currentValue === "custom" ? [this.optionOf(CUSTOM_PRESET)] : []],
|
|
226
|
+
currentValue
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Resolve a preset's knob bundle.
|
|
231
|
+
* @param name - the preset name to resolve.
|
|
232
|
+
* @returns the configured bundle.
|
|
233
|
+
* @throws when `name` is not in the table.
|
|
234
|
+
*/
|
|
235
|
+
resolve(name) {
|
|
236
|
+
const spec = this.presets[name];
|
|
237
|
+
if (spec === void 0) throw new Error(`permission: unknown preset "${name}" (known: ${Object.keys(this.presets).join(", ")})`);
|
|
238
|
+
return spec;
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* Build the client option for a table entry or {@link CUSTOM_PRESET}. A
|
|
242
|
+
* missing label falls back to the table key.
|
|
243
|
+
* @param name - a table key, or `custom`.
|
|
244
|
+
* @returns the option a client renders.
|
|
245
|
+
* @throws when `name` is neither a table key nor `custom`.
|
|
246
|
+
*/
|
|
247
|
+
optionOf(name) {
|
|
248
|
+
if (name === "custom") return {
|
|
249
|
+
value: CUSTOM_PRESET,
|
|
250
|
+
name: "Custom",
|
|
251
|
+
description: "Current sandbox and approval settings do not match a preset."
|
|
252
|
+
};
|
|
253
|
+
const spec = this.resolve(name);
|
|
254
|
+
return {
|
|
255
|
+
value: name,
|
|
256
|
+
name: spec.name ?? name,
|
|
257
|
+
...spec.description !== void 0 ? { description: spec.description } : {}
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Record a changed preset, then update each changed knob through its own
|
|
262
|
+
* setter. Selecting the effective preset again appends nothing.
|
|
263
|
+
* @param session - the session the switch belongs to.
|
|
264
|
+
* @param name - the preset to switch to; unknown names throw.
|
|
265
|
+
*/
|
|
266
|
+
set(session, name) {
|
|
267
|
+
this.apply(session, name, (policy) => {
|
|
268
|
+
setApprovalPolicy(session, policy);
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
/** Apply one preset with the caller-selected live or initialization policy writer. */
|
|
272
|
+
apply(session, name, setApproval) {
|
|
273
|
+
const spec = this.resolve(name);
|
|
274
|
+
if (this.current(session.events) !== name) session.append("permission/preset", { preset: name });
|
|
275
|
+
const events = session.events;
|
|
276
|
+
if (spec.sandbox !== (effectiveSandboxMode(events) ?? this.ctx.shell.sandboxMode)) setSandboxMode(session, spec.sandbox);
|
|
277
|
+
if (spec.approval !== (effectiveApprovalPolicy(events) ?? this.ctx.approval.config.policy ?? "ask")) setApproval(spec.approval);
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Fill every missing permission fact before a session is published. A
|
|
281
|
+
* genuinely fresh session uses the current user default; seeded or partially
|
|
282
|
+
* initialized sessions preserve their effective knob values and only gain
|
|
283
|
+
* the missing durable facts.
|
|
284
|
+
*/
|
|
285
|
+
pinInitialPermission(session) {
|
|
286
|
+
const events = session.events;
|
|
287
|
+
const selected = effectivePermissionPreset(events);
|
|
288
|
+
const sandbox = effectiveSandboxMode(events);
|
|
289
|
+
const approval = effectiveApprovalPolicy(events);
|
|
290
|
+
const seeded = events.some((event) => event.type === "session/end-seed");
|
|
291
|
+
if (selected === void 0 && sandbox === void 0 && approval === void 0 && !seeded) {
|
|
292
|
+
const name = this.defaultPreset;
|
|
293
|
+
const spec = this.resolve(name);
|
|
294
|
+
session.append("permission/preset", { preset: name });
|
|
295
|
+
setSandboxMode(session, spec.sandbox);
|
|
296
|
+
setApprovalPolicy(session, spec.approval);
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const state = {
|
|
300
|
+
preset: selected ?? null,
|
|
301
|
+
sandbox: sandbox ?? null,
|
|
302
|
+
approval: approval ?? null
|
|
303
|
+
};
|
|
304
|
+
const effective = this.derive(state);
|
|
305
|
+
if (selected === void 0 && effective !== "custom") session.append("permission/preset", { preset: effective });
|
|
306
|
+
if (sandbox === void 0) setSandboxMode(session, this.ctx.shell.sandboxMode);
|
|
307
|
+
if (approval === void 0) setApprovalPolicy(session, this.ctx.approval.config.policy ?? "ask");
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
//#endregion
|
|
311
|
+
export { CUSTOM_PRESET, PERMISSION_SETTINGS_NAMESPACE, PermissionPresetService, PermissionPresetService as default, applyKnobEvent, effectivePermissionPreset };
|
package/lib/invariant.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region lib/types/invariant.js
|
|
2
|
+
/** Package-owned permission-preset event invariants. @module @stackstackstack/dsh-permission-presets/invariant */
|
|
3
|
+
const PACKAGE_NAME = "@stackstackstack/dsh-permission-presets";
|
|
4
|
+
/** Cordis companion plugin name. */
|
|
5
|
+
const name = "permission-presets-invariant";
|
|
6
|
+
/** Service required before the companion can reserve package ownership. */
|
|
7
|
+
const inject = ["invariants"];
|
|
8
|
+
/** Validate the package-owned event fields and ignore unrelated events. */
|
|
9
|
+
function validateEvent(ctx, event, fail) {
|
|
10
|
+
if (event.type === "permission/preset" && !ctx.permissionPresets.names.includes(event.data.preset)) fail(`permission/preset names unknown preset ${JSON.stringify(event.data.preset)}`);
|
|
11
|
+
}
|
|
12
|
+
/** Install validation that loaded and newly appended preset events remain resolvable. */
|
|
13
|
+
const install = Object.assign((ctx, fail) => {
|
|
14
|
+
for (const session of ctx.sessions.list()) for (const event of session.events) validateEvent(ctx, event, fail);
|
|
15
|
+
ctx.on("internal/dispatch", (_mode, eventName, args) => {
|
|
16
|
+
if (eventName !== "session/event") return;
|
|
17
|
+
const event = args[1];
|
|
18
|
+
validateEvent(ctx, event, fail);
|
|
19
|
+
}, { global: true });
|
|
20
|
+
}, { inject: ["permissionPresets", "sessions"] });
|
|
21
|
+
/**
|
|
22
|
+
* Register the permission invariant companion.
|
|
23
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
24
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
25
|
+
*/
|
|
26
|
+
const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
27
|
+
//#endregion
|
|
28
|
+
export { apply, inject, name };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-namespace projection of the permission domain: a pure re-export of
|
|
3
|
+
* the package's types outlet. Client code imports ONLY the client namespace
|
|
4
|
+
* (repo discipline), so `./client` projects the same single-source content
|
|
5
|
+
* `./types` serves to host consumers — zero duplication.
|
|
6
|
+
*
|
|
7
|
+
* @module @stackstackstack/dsh-permission-presets/client
|
|
8
|
+
*/
|
|
9
|
+
export type * from './types.ts';
|
|
10
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-namespace projection of the permission domain: a pure re-export of
|
|
3
|
+
* the package's types outlet. Client code imports ONLY the client namespace
|
|
4
|
+
* (repo discipline), so `./client` projects the same single-source content
|
|
5
|
+
* `./types` serves to host consumers — zero duplication.
|
|
6
|
+
*
|
|
7
|
+
* @module @stackstackstack/dsh-permission-presets/client
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
10
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing permission presets over the independent sandbox-mode and
|
|
3
|
+
* approval-policy knobs. A switch records the selected preset, then writes
|
|
4
|
+
* changed knobs through their canonical setters. Execution, prompt narration,
|
|
5
|
+
* and replay keep reading their knob folds. The preset event preserves user
|
|
6
|
+
* intent when two presets share a bundle. The read side ships as the
|
|
7
|
+
* `permissions` session projection; the write side ships as the
|
|
8
|
+
* `/permission` command — both optional children over the same service.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-permission-presets
|
|
11
|
+
*/
|
|
12
|
+
import { Context, Service } from '@deepseek-ai/cordis';
|
|
13
|
+
import z from '@deepseek-ai/schemastery';
|
|
14
|
+
import type { Session, SessionEvent } from '@stackstackstack/dsh-session';
|
|
15
|
+
import type { SandboxMode } from '@stackstackstack/dsh-sandbox';
|
|
16
|
+
import type { ApprovalPolicy } from '@stackstackstack/dsh-user-approval';
|
|
17
|
+
import type { PermissionSelect, PresetOption } from './types.ts';
|
|
18
|
+
export type * from './types.ts';
|
|
19
|
+
declare module '@deepseek-ai/cordis' {
|
|
20
|
+
interface Context {
|
|
21
|
+
permissionPresets: PermissionPresetService;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
declare module '@stackstackstack/dsh-session/types' {
|
|
25
|
+
interface SessionEventMap {
|
|
26
|
+
/**
|
|
27
|
+
* Records the selected preset as durable, log-only user intent. The knob
|
|
28
|
+
* events follow in the same turn and control execution; this event stays
|
|
29
|
+
* out of the model transcript and lets {@link effectivePermissionPreset}
|
|
30
|
+
* preserve a selection when bundles match.
|
|
31
|
+
*/
|
|
32
|
+
'permission/preset': {
|
|
33
|
+
preset: string;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/** One preset's sandbox/approval bundle and optional client presentation. */
|
|
38
|
+
export interface PresetSpec {
|
|
39
|
+
/** The `sandbox/mode` value the preset writes through. */
|
|
40
|
+
sandbox: SandboxMode;
|
|
41
|
+
/** The `approval/policy` value the preset writes through. */
|
|
42
|
+
approval: ApprovalPolicy;
|
|
43
|
+
/** The display label a client shows for this preset; the raw table key when omitted. */
|
|
44
|
+
name?: string;
|
|
45
|
+
/** One user-facing sentence on what the preset means; omitted when not configured. */
|
|
46
|
+
description?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Returned when effective knob values match no table entry. Clients may show
|
|
50
|
+
* it as the current value, but it is never a switch target or event payload.
|
|
51
|
+
*/
|
|
52
|
+
export declare const CUSTOM_PRESET = "custom";
|
|
53
|
+
/** Settings namespace carrying the default for future sessions. */
|
|
54
|
+
export declare const PERMISSION_SETTINGS_NAMESPACE: import("@stackstackstack/dsh-settings").SettingsNamespace;
|
|
55
|
+
/**
|
|
56
|
+
* Fold the last selected preset from the durable log; replay needs no catch-up
|
|
57
|
+
* state.
|
|
58
|
+
* @param events - session events in log order; other event types are ignored.
|
|
59
|
+
* @returns the last selected preset, or undefined when none was recorded.
|
|
60
|
+
*/
|
|
61
|
+
export declare function effectivePermissionPreset(events: readonly SessionEvent[]): string | undefined;
|
|
62
|
+
/**
|
|
63
|
+
* The projection unit's state: the last seen value of each knob event, null
|
|
64
|
+
* before an override (composition defaults apply at view time). Plain JSON
|
|
65
|
+
* (persisted-cache precondition).
|
|
66
|
+
*/
|
|
67
|
+
export interface KnobState {
|
|
68
|
+
/** Last `permission/preset` payload, or null. */
|
|
69
|
+
preset: string | null;
|
|
70
|
+
/** Last `sandbox/mode` payload, or null. */
|
|
71
|
+
sandbox: SandboxMode | null;
|
|
72
|
+
/** Last `approval/policy` payload, or null. */
|
|
73
|
+
approval: ApprovalPolicy | null;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* One-event knob transition (the projection unit's `apply`). Uninterested
|
|
77
|
+
* events return the same reference — the registry's change gate.
|
|
78
|
+
* @param state - the folded knob state before `event`.
|
|
79
|
+
* @param event - one committed session event.
|
|
80
|
+
* @returns the next state; the same reference when the event is not a knob.
|
|
81
|
+
*/
|
|
82
|
+
export declare function applyKnobEvent(state: KnobState, event: SessionEvent): KnobState;
|
|
83
|
+
/** User setting resolved when a new session receives its initial permission. */
|
|
84
|
+
export interface PermissionSettings {
|
|
85
|
+
/** Preset pinned into a newly created session. */
|
|
86
|
+
defaultPreset: string;
|
|
87
|
+
}
|
|
88
|
+
/** The {@link PermissionPresetService} config: preset table and composition default. */
|
|
89
|
+
export interface Config {
|
|
90
|
+
/**
|
|
91
|
+
* The preset table: name → knob bundle. Defaults to `workspace-write`
|
|
92
|
+
* (workspace-write + ask) and `danger-full-access` (danger-full-access +
|
|
93
|
+
* never). The name `custom` is reserved for the derived not-a-preset state.
|
|
94
|
+
*/
|
|
95
|
+
presets?: Record<string, PresetSpec>;
|
|
96
|
+
/**
|
|
97
|
+
* Default for new sessions. When omitted, the preset matching the composed
|
|
98
|
+
* sandbox and approval defaults is used.
|
|
99
|
+
*/
|
|
100
|
+
defaultPreset?: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Owns the deployment's permission presets and their write path. Requires a
|
|
104
|
+
* confining `ctx.shell` executor and `ctx.approval`; unmatched knob values are
|
|
105
|
+
* reported as {@link CUSTOM_PRESET}, not an error.
|
|
106
|
+
*/
|
|
107
|
+
export declare class PermissionPresetService extends Service {
|
|
108
|
+
static Config: z<Config>;
|
|
109
|
+
static inject: string[];
|
|
110
|
+
private readonly presets;
|
|
111
|
+
private defaultSettings;
|
|
112
|
+
constructor(ctx: Context, config: Config);
|
|
113
|
+
/**
|
|
114
|
+
* The advertised preset names, in the preset table's declaration order.
|
|
115
|
+
* @returns every switchable preset name.
|
|
116
|
+
*/
|
|
117
|
+
get names(): readonly string[];
|
|
118
|
+
/**
|
|
119
|
+
* The preset currently selected as the default for future sessions.
|
|
120
|
+
* @returns the resolved settings value, or the composition default without
|
|
121
|
+
* a mounted settings provider.
|
|
122
|
+
*/
|
|
123
|
+
get defaultPreset(): string;
|
|
124
|
+
/**
|
|
125
|
+
* Resolve the preset matching the effective knob values. A still-matching
|
|
126
|
+
* last selection wins shared-bundle ties; otherwise the first table match
|
|
127
|
+
* wins, or {@link CUSTOM_PRESET} when no entry matches.
|
|
128
|
+
* @param events - the session's events in log order.
|
|
129
|
+
* @returns the effective preset name, or `custom` when nothing matches.
|
|
130
|
+
*/
|
|
131
|
+
current(events: readonly SessionEvent[]): string;
|
|
132
|
+
/** Resolve the preset for one folded knob state (the shared mathematics of `current` and the projection unit). */
|
|
133
|
+
private derive;
|
|
134
|
+
/**
|
|
135
|
+
* Build the whole select value for one folded knob state: every table
|
|
136
|
+
* option in declaration order, `custom` appended exactly while derived.
|
|
137
|
+
* @param state - the folded knob overrides.
|
|
138
|
+
* @returns the `permissions` projection payload.
|
|
139
|
+
*/
|
|
140
|
+
selectFor(state: KnobState): PermissionSelect;
|
|
141
|
+
/**
|
|
142
|
+
* Resolve a preset's knob bundle.
|
|
143
|
+
* @param name - the preset name to resolve.
|
|
144
|
+
* @returns the configured bundle.
|
|
145
|
+
* @throws when `name` is not in the table.
|
|
146
|
+
*/
|
|
147
|
+
resolve(name: string): PresetSpec;
|
|
148
|
+
/**
|
|
149
|
+
* Build the client option for a table entry or {@link CUSTOM_PRESET}. A
|
|
150
|
+
* missing label falls back to the table key.
|
|
151
|
+
* @param name - a table key, or `custom`.
|
|
152
|
+
* @returns the option a client renders.
|
|
153
|
+
* @throws when `name` is neither a table key nor `custom`.
|
|
154
|
+
*/
|
|
155
|
+
optionOf(name: string): PresetOption;
|
|
156
|
+
/**
|
|
157
|
+
* Record a changed preset, then update each changed knob through its own
|
|
158
|
+
* setter. Selecting the effective preset again appends nothing.
|
|
159
|
+
* @param session - the session the switch belongs to.
|
|
160
|
+
* @param name - the preset to switch to; unknown names throw.
|
|
161
|
+
*/
|
|
162
|
+
set(session: Session, name: string): void;
|
|
163
|
+
/** Apply one preset with the caller-selected live or initialization policy writer. */
|
|
164
|
+
private apply;
|
|
165
|
+
/**
|
|
166
|
+
* Fill every missing permission fact before a session is published. A
|
|
167
|
+
* genuinely fresh session uses the current user default; seeded or partially
|
|
168
|
+
* initialized sessions preserve their effective knob values and only gain
|
|
169
|
+
* the missing durable facts.
|
|
170
|
+
*/
|
|
171
|
+
private pinInitialPermission;
|
|
172
|
+
}
|
|
173
|
+
export default PermissionPresetService;
|
|
174
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing permission presets over the independent sandbox-mode and
|
|
3
|
+
* approval-policy knobs. A switch records the selected preset, then writes
|
|
4
|
+
* changed knobs through their canonical setters. Execution, prompt narration,
|
|
5
|
+
* and replay keep reading their knob folds. The preset event preserves user
|
|
6
|
+
* intent when two presets share a bundle. The read side ships as the
|
|
7
|
+
* `permissions` session projection; the write side ships as the
|
|
8
|
+
* `/permission` command — both optional children over the same service.
|
|
9
|
+
*
|
|
10
|
+
* @module dsh-permission-presets
|
|
11
|
+
*/
|
|
12
|
+
import { Service } from '@deepseek-ai/cordis';
|
|
13
|
+
import z from '@deepseek-ai/schemastery';
|
|
14
|
+
import { z as zod } from 'zod';
|
|
15
|
+
import { SANDBOX_MODES, effectiveSandboxMode, setSandboxMode } from '@stackstackstack/dsh-sandbox-policy';
|
|
16
|
+
import { APPROVAL_POLICIES, effectiveApprovalPolicy, setApprovalPolicy } from '@stackstackstack/dsh-user-approval';
|
|
17
|
+
import { installSettingsSection, settingsNamespace } from '@stackstackstack/dsh-settings';
|
|
18
|
+
/**
|
|
19
|
+
* Returned when effective knob values match no table entry. Clients may show
|
|
20
|
+
* it as the current value, but it is never a switch target or event payload.
|
|
21
|
+
*/
|
|
22
|
+
export const CUSTOM_PRESET = 'custom';
|
|
23
|
+
/** Settings namespace carrying the default for future sessions. */
|
|
24
|
+
export const PERMISSION_SETTINGS_NAMESPACE = settingsNamespace('permission');
|
|
25
|
+
/**
|
|
26
|
+
* Fold the last selected preset from the durable log; replay needs no catch-up
|
|
27
|
+
* state.
|
|
28
|
+
* @param events - session events in log order; other event types are ignored.
|
|
29
|
+
* @returns the last selected preset, or undefined when none was recorded.
|
|
30
|
+
*/
|
|
31
|
+
export function effectivePermissionPreset(events) {
|
|
32
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
33
|
+
const event = events[index];
|
|
34
|
+
if (event.type === 'permission/preset')
|
|
35
|
+
return event.data.preset;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
/** State for the empty log: every knob at its composition default. */
|
|
40
|
+
const EMPTY_KNOBS = { preset: null, sandbox: null, approval: null };
|
|
41
|
+
/**
|
|
42
|
+
* One-event knob transition (the projection unit's `apply`). Uninterested
|
|
43
|
+
* events return the same reference — the registry's change gate.
|
|
44
|
+
* @param state - the folded knob state before `event`.
|
|
45
|
+
* @param event - one committed session event.
|
|
46
|
+
* @returns the next state; the same reference when the event is not a knob.
|
|
47
|
+
*/
|
|
48
|
+
export function applyKnobEvent(state, event) {
|
|
49
|
+
switch (event.type) {
|
|
50
|
+
case 'permission/preset':
|
|
51
|
+
return { ...state, preset: event.data.preset };
|
|
52
|
+
case 'sandbox/mode':
|
|
53
|
+
return { ...state, sandbox: event.data.mode };
|
|
54
|
+
case 'approval/policy':
|
|
55
|
+
return { ...state, approval: event.data.policy };
|
|
56
|
+
default:
|
|
57
|
+
return state;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
/** Whole-log knob fold (the cold-read parallel of {@link applyKnobEvent}). */
|
|
61
|
+
function foldKnobs(events) {
|
|
62
|
+
let state = EMPTY_KNOBS;
|
|
63
|
+
for (const event of events)
|
|
64
|
+
state = applyKnobEvent(state, event);
|
|
65
|
+
return state;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Owns the deployment's permission presets and their write path. Requires a
|
|
69
|
+
* confining `ctx.shell` executor and `ctx.approval`; unmatched knob values are
|
|
70
|
+
* reported as {@link CUSTOM_PRESET}, not an error.
|
|
71
|
+
*/
|
|
72
|
+
export class PermissionPresetService extends Service {
|
|
73
|
+
// Inline schema call: the config catalog walks `static Config` statically.
|
|
74
|
+
static Config = z.object({
|
|
75
|
+
presets: z.dict(z.object({
|
|
76
|
+
sandbox: z.union(SANDBOX_MODES).required(),
|
|
77
|
+
approval: z.union(APPROVAL_POLICIES).required(),
|
|
78
|
+
name: z.string(),
|
|
79
|
+
description: z.string(),
|
|
80
|
+
})).default({
|
|
81
|
+
'workspace-write': {
|
|
82
|
+
sandbox: 'workspace-write', approval: 'ask',
|
|
83
|
+
name: 'workspace-write', description: 'Write inside the workspace and permitted temporary directories; wider retries require approval.',
|
|
84
|
+
},
|
|
85
|
+
'danger-full-access': {
|
|
86
|
+
sandbox: 'danger-full-access', approval: 'never',
|
|
87
|
+
name: 'danger-full-access', description: 'Full file access without approval prompts.',
|
|
88
|
+
},
|
|
89
|
+
}),
|
|
90
|
+
defaultPreset: z.string(),
|
|
91
|
+
});
|
|
92
|
+
static inject = ['shell', 'approval', 'sessions'];
|
|
93
|
+
presets;
|
|
94
|
+
defaultSettings;
|
|
95
|
+
constructor(ctx, config) {
|
|
96
|
+
super(ctx, 'permissionPresets');
|
|
97
|
+
// The schema defaulted the table — the cast records that runtime fact.
|
|
98
|
+
this.presets = config.presets;
|
|
99
|
+
if (CUSTOM_PRESET in this.presets) {
|
|
100
|
+
throw new Error(`permission: "${CUSTOM_PRESET}" is reserved for the derived not-a-preset state and cannot name a table entry`);
|
|
101
|
+
}
|
|
102
|
+
if (ctx.shell.sandboxMode === undefined) {
|
|
103
|
+
throw new Error('permission: the mounted bash executor does not confine (no sandboxMode) — presets bundle a sandbox mode, so composing this plugin over an unconfined executor is a misconfiguration');
|
|
104
|
+
}
|
|
105
|
+
const inferredDefault = this.derive(EMPTY_KNOBS);
|
|
106
|
+
const defaultPreset = config.defaultPreset ?? inferredDefault;
|
|
107
|
+
if (defaultPreset === CUSTOM_PRESET) {
|
|
108
|
+
throw new Error('permission: composed sandbox and approval defaults match no preset; configure defaultPreset explicitly');
|
|
109
|
+
}
|
|
110
|
+
this.resolve(defaultPreset);
|
|
111
|
+
const baseSettings = { defaultPreset };
|
|
112
|
+
this.defaultSettings = () => baseSettings;
|
|
113
|
+
const presetChoices = this.names.map((name) => {
|
|
114
|
+
const choice = z.const(name);
|
|
115
|
+
const label = this.presets[name]?.name;
|
|
116
|
+
return label === undefined ? choice : choice.description(label);
|
|
117
|
+
});
|
|
118
|
+
const settingsSchema = z.object({
|
|
119
|
+
defaultPreset: z.union(presetChoices).required(),
|
|
120
|
+
});
|
|
121
|
+
installSettingsSection(ctx, PERMISSION_SETTINGS_NAMESPACE, settingsSchema, baseSettings, {
|
|
122
|
+
setSource: (current) => {
|
|
123
|
+
this.defaultSettings = current;
|
|
124
|
+
},
|
|
125
|
+
// The source thunk reads the latest scope snapshot at session creation;
|
|
126
|
+
// no process-level registration needs replacement on change.
|
|
127
|
+
onChange: () => { },
|
|
128
|
+
});
|
|
129
|
+
ctx.on('session/created', (session) => {
|
|
130
|
+
this.pinInitialPermission(session);
|
|
131
|
+
});
|
|
132
|
+
for (const session of ctx.sessions.list()) {
|
|
133
|
+
this.pinInitialPermission(session);
|
|
134
|
+
}
|
|
135
|
+
// The permissions projection unit: fold the three whole-value knob
|
|
136
|
+
// events; view derives the select over the composition defaults this
|
|
137
|
+
// service already owns. The unit child activates only when a projection
|
|
138
|
+
// registry is composed (headless assemblies stay unaffected).
|
|
139
|
+
// zod `.optional()` types the key `string | undefined` while the domain
|
|
140
|
+
// says `description?: string`; on the JSON wire the two serialize
|
|
141
|
+
// identically (absent), so the cast records exactly that
|
|
142
|
+
// exactOptionalPropertyTypes widening (the Wire<T> precedent).
|
|
143
|
+
const selectSchema = zod.object({
|
|
144
|
+
options: zod.array(zod.object({
|
|
145
|
+
value: zod.string().min(1),
|
|
146
|
+
name: zod.string().min(1),
|
|
147
|
+
description: zod.string().optional(),
|
|
148
|
+
})),
|
|
149
|
+
currentValue: zod.string().min(1),
|
|
150
|
+
});
|
|
151
|
+
ctx.inject(['sessionProjections'], (projectionCtx) => {
|
|
152
|
+
projectionCtx.sessionProjections.register({
|
|
153
|
+
key: 'permissions',
|
|
154
|
+
schema: selectSchema,
|
|
155
|
+
init: () => EMPTY_KNOBS,
|
|
156
|
+
apply: applyKnobEvent,
|
|
157
|
+
view: state => this.selectFor(state),
|
|
158
|
+
stateVersion: 1,
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
// The /permission command: the one write path a web client uses (the
|
|
162
|
+
// popup contribution submits the picked preset as this line). The child
|
|
163
|
+
// activates only when a command registry is composed.
|
|
164
|
+
ctx.inject(['commands'], (commandCtx) => {
|
|
165
|
+
commandCtx.commands.register({
|
|
166
|
+
name: 'permission',
|
|
167
|
+
description: 'Switch the permission preset (sandbox mode + approval policy)',
|
|
168
|
+
input: { hint: '<preset>' },
|
|
169
|
+
// No settlement text labels its value with this command's own name: a
|
|
170
|
+
// surface that renders `name · text` (the web command row) would
|
|
171
|
+
// otherwise read `permission · Permission preset: workspace-write.`
|
|
172
|
+
handler: ({ agent, rawInput }) => {
|
|
173
|
+
const name = rawInput.trim();
|
|
174
|
+
if (name === '') {
|
|
175
|
+
return { kind: 'success', text: `current preset ${this.current(agent.session.events)} (available: ${this.names.join(', ')})` };
|
|
176
|
+
}
|
|
177
|
+
if (!this.names.includes(name)) {
|
|
178
|
+
return { kind: 'error', text: `unknown preset "${name}" (available: ${this.names.join(', ')})` };
|
|
179
|
+
}
|
|
180
|
+
this.apply(agent.session, name, (policy) => { this.ctx.approval.setPolicy(agent, policy); });
|
|
181
|
+
return { kind: 'success', text: `preset ${name}` };
|
|
182
|
+
},
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* The advertised preset names, in the preset table's declaration order.
|
|
188
|
+
* @returns every switchable preset name.
|
|
189
|
+
*/
|
|
190
|
+
get names() {
|
|
191
|
+
return Object.keys(this.presets);
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* The preset currently selected as the default for future sessions.
|
|
195
|
+
* @returns the resolved settings value, or the composition default without
|
|
196
|
+
* a mounted settings provider.
|
|
197
|
+
*/
|
|
198
|
+
get defaultPreset() {
|
|
199
|
+
return this.defaultSettings().defaultPreset;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Resolve the preset matching the effective knob values. A still-matching
|
|
203
|
+
* last selection wins shared-bundle ties; otherwise the first table match
|
|
204
|
+
* wins, or {@link CUSTOM_PRESET} when no entry matches.
|
|
205
|
+
* @param events - the session's events in log order.
|
|
206
|
+
* @returns the effective preset name, or `custom` when nothing matches.
|
|
207
|
+
*/
|
|
208
|
+
current(events) {
|
|
209
|
+
return this.derive(foldKnobs(events));
|
|
210
|
+
}
|
|
211
|
+
/** Resolve the preset for one folded knob state (the shared mathematics of `current` and the projection unit). */
|
|
212
|
+
derive(state) {
|
|
213
|
+
const sandbox = state.sandbox ?? this.ctx.shell.sandboxMode;
|
|
214
|
+
const approval = state.approval ?? this.ctx.approval.config.policy ?? 'ask';
|
|
215
|
+
const matches = (spec) => spec.sandbox === sandbox && spec.approval === approval;
|
|
216
|
+
if (state.preset !== null) {
|
|
217
|
+
const spec = this.presets[state.preset];
|
|
218
|
+
if (spec !== undefined && matches(spec))
|
|
219
|
+
return state.preset;
|
|
220
|
+
}
|
|
221
|
+
for (const [name, spec] of Object.entries(this.presets)) {
|
|
222
|
+
if (matches(spec))
|
|
223
|
+
return name;
|
|
224
|
+
}
|
|
225
|
+
return CUSTOM_PRESET;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Build the whole select value for one folded knob state: every table
|
|
229
|
+
* option in declaration order, `custom` appended exactly while derived.
|
|
230
|
+
* @param state - the folded knob overrides.
|
|
231
|
+
* @returns the `permissions` projection payload.
|
|
232
|
+
*/
|
|
233
|
+
selectFor(state) {
|
|
234
|
+
const currentValue = this.derive(state);
|
|
235
|
+
return {
|
|
236
|
+
options: [
|
|
237
|
+
...this.names.map(name => this.optionOf(name)),
|
|
238
|
+
...currentValue === CUSTOM_PRESET ? [this.optionOf(CUSTOM_PRESET)] : [],
|
|
239
|
+
],
|
|
240
|
+
currentValue,
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Resolve a preset's knob bundle.
|
|
245
|
+
* @param name - the preset name to resolve.
|
|
246
|
+
* @returns the configured bundle.
|
|
247
|
+
* @throws when `name` is not in the table.
|
|
248
|
+
*/
|
|
249
|
+
resolve(name) {
|
|
250
|
+
const spec = this.presets[name];
|
|
251
|
+
if (spec === undefined) {
|
|
252
|
+
throw new Error(`permission: unknown preset "${name}" (known: ${Object.keys(this.presets).join(', ')})`);
|
|
253
|
+
}
|
|
254
|
+
return spec;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Build the client option for a table entry or {@link CUSTOM_PRESET}. A
|
|
258
|
+
* missing label falls back to the table key.
|
|
259
|
+
* @param name - a table key, or `custom`.
|
|
260
|
+
* @returns the option a client renders.
|
|
261
|
+
* @throws when `name` is neither a table key nor `custom`.
|
|
262
|
+
*/
|
|
263
|
+
optionOf(name) {
|
|
264
|
+
if (name === CUSTOM_PRESET) {
|
|
265
|
+
return { value: CUSTOM_PRESET, name: 'Custom', description: 'Current sandbox and approval settings do not match a preset.' };
|
|
266
|
+
}
|
|
267
|
+
const spec = this.resolve(name);
|
|
268
|
+
return { value: name, name: spec.name ?? name, ...spec.description !== undefined ? { description: spec.description } : {} };
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Record a changed preset, then update each changed knob through its own
|
|
272
|
+
* setter. Selecting the effective preset again appends nothing.
|
|
273
|
+
* @param session - the session the switch belongs to.
|
|
274
|
+
* @param name - the preset to switch to; unknown names throw.
|
|
275
|
+
*/
|
|
276
|
+
set(session, name) {
|
|
277
|
+
this.apply(session, name, (policy) => { setApprovalPolicy(session, policy); });
|
|
278
|
+
}
|
|
279
|
+
/** Apply one preset with the caller-selected live or initialization policy writer. */
|
|
280
|
+
apply(session, name, setApproval) {
|
|
281
|
+
const spec = this.resolve(name);
|
|
282
|
+
if (this.current(session.events) !== name) {
|
|
283
|
+
session.append('permission/preset', { preset: name });
|
|
284
|
+
}
|
|
285
|
+
const events = session.events;
|
|
286
|
+
if (spec.sandbox !== (effectiveSandboxMode(events) ?? this.ctx.shell.sandboxMode)) {
|
|
287
|
+
setSandboxMode(session, spec.sandbox);
|
|
288
|
+
}
|
|
289
|
+
if (spec.approval !== (effectiveApprovalPolicy(events) ?? this.ctx.approval.config.policy ?? 'ask')) {
|
|
290
|
+
setApproval(spec.approval);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Fill every missing permission fact before a session is published. A
|
|
295
|
+
* genuinely fresh session uses the current user default; seeded or partially
|
|
296
|
+
* initialized sessions preserve their effective knob values and only gain
|
|
297
|
+
* the missing durable facts.
|
|
298
|
+
*/
|
|
299
|
+
pinInitialPermission(session) {
|
|
300
|
+
const events = session.events;
|
|
301
|
+
const selected = effectivePermissionPreset(events);
|
|
302
|
+
const sandbox = effectiveSandboxMode(events);
|
|
303
|
+
const approval = effectiveApprovalPolicy(events);
|
|
304
|
+
const seeded = events.some(event => event.type === 'session/end-seed');
|
|
305
|
+
if (selected === undefined && sandbox === undefined && approval === undefined && !seeded) {
|
|
306
|
+
const name = this.defaultPreset;
|
|
307
|
+
const spec = this.resolve(name);
|
|
308
|
+
session.append('permission/preset', { preset: name });
|
|
309
|
+
setSandboxMode(session, spec.sandbox);
|
|
310
|
+
setApprovalPolicy(session, spec.approval);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
const state = {
|
|
314
|
+
preset: selected ?? null,
|
|
315
|
+
sandbox: sandbox ?? null,
|
|
316
|
+
approval: approval ?? null,
|
|
317
|
+
};
|
|
318
|
+
const effective = this.derive(state);
|
|
319
|
+
if (selected === undefined && effective !== CUSTOM_PRESET) {
|
|
320
|
+
session.append('permission/preset', { preset: effective });
|
|
321
|
+
}
|
|
322
|
+
if (sandbox === undefined) {
|
|
323
|
+
setSandboxMode(session, this.ctx.shell.sandboxMode);
|
|
324
|
+
}
|
|
325
|
+
if (approval === undefined) {
|
|
326
|
+
setApprovalPolicy(session, this.ctx.approval.config.policy ?? 'ask');
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
export default PermissionPresetService;
|
|
331
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Package-owned permission-preset event invariants. @module @stackstackstack/dsh-permission-presets/invariant */
|
|
2
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
export declare const name = "permission-presets-invariant";
|
|
5
|
+
/** Service required before the companion can reserve package ownership. */
|
|
6
|
+
export declare const inject: string[];
|
|
7
|
+
/**
|
|
8
|
+
* Register the permission invariant companion.
|
|
9
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
10
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
11
|
+
*/
|
|
12
|
+
export declare const apply: (ctx: Context) => Promise<() => void>;
|
|
13
|
+
//# sourceMappingURL=invariant.d.ts.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Package-owned permission-preset event invariants. @module @stackstackstack/dsh-permission-presets/invariant */
|
|
2
|
+
const PACKAGE_NAME = '@stackstackstack/dsh-permission-presets';
|
|
3
|
+
/** Cordis companion plugin name. */
|
|
4
|
+
export const name = 'permission-presets-invariant';
|
|
5
|
+
/** Service required before the companion can reserve package ownership. */
|
|
6
|
+
export const inject = ['invariants'];
|
|
7
|
+
/** Validate the package-owned event fields and ignore unrelated events. */
|
|
8
|
+
function validateEvent(ctx, event, fail) {
|
|
9
|
+
if (event.type === 'permission/preset' && !ctx.permissionPresets.names.includes(event.data.preset)) {
|
|
10
|
+
fail(`permission/preset names unknown preset ${JSON.stringify(event.data.preset)}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Install validation that loaded and newly appended preset events remain resolvable. */
|
|
14
|
+
const install = Object.assign((ctx, fail) => {
|
|
15
|
+
for (const session of ctx.sessions.list()) {
|
|
16
|
+
for (const event of session.events)
|
|
17
|
+
validateEvent(ctx, event, fail);
|
|
18
|
+
}
|
|
19
|
+
ctx.on('internal/dispatch', (_mode, eventName, args) => {
|
|
20
|
+
if (eventName !== 'session/event')
|
|
21
|
+
return;
|
|
22
|
+
const event = args[1];
|
|
23
|
+
validateEvent(ctx, event, fail);
|
|
24
|
+
}, { global: true });
|
|
25
|
+
}, { inject: ['permissionPresets', 'sessions'] });
|
|
26
|
+
/**
|
|
27
|
+
* Register the permission invariant companion.
|
|
28
|
+
* @param ctx - Cordis context carrying the invariant service.
|
|
29
|
+
* @returns the installed registration's disposer after setup succeeds.
|
|
30
|
+
*/
|
|
31
|
+
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
|
|
32
|
+
//# sourceMappingURL=invariant.js.map
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure types of the permission domain: the ONE home of the `permissions`
|
|
3
|
+
* projection-key declaration plus its payload types, free of this package's
|
|
4
|
+
* host-side value imports (cordis, schemastery). Two namespace projections
|
|
5
|
+
* serve it — the package root re-export for host consumers, `./client` (the
|
|
6
|
+
* browser half-entry's re-export) for client aggregates — with zero content
|
|
7
|
+
* duplication.
|
|
8
|
+
*
|
|
9
|
+
* @module @stackstackstack/dsh-permission-presets/types
|
|
10
|
+
*/
|
|
11
|
+
/** The select-option shape a presentation layer advertises for one preset (or for the derived `custom` state). */
|
|
12
|
+
export interface PresetOption {
|
|
13
|
+
/** Stable option value: the table key, or `custom`. */
|
|
14
|
+
value: string;
|
|
15
|
+
/** The display label. */
|
|
16
|
+
name: string;
|
|
17
|
+
/** One user-facing sentence on what the value means; omitted when not configured. */
|
|
18
|
+
description?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Whole `permissions` projection value: every switchable preset in table
|
|
22
|
+
* order (plus the derived current-only `custom` when the knobs match no
|
|
23
|
+
* entry) and the effective current value.
|
|
24
|
+
*/
|
|
25
|
+
export interface PermissionSelect {
|
|
26
|
+
/** Switchable presets, plus `custom` appended exactly while it is current. */
|
|
27
|
+
options: PresetOption[];
|
|
28
|
+
/** The effective current value: a preset table key, or `custom`. */
|
|
29
|
+
currentValue: string;
|
|
30
|
+
}
|
|
31
|
+
declare module '@stackstackstack/dsh-session-projection/types' {
|
|
32
|
+
interface SessionProjectionMap {
|
|
33
|
+
/**
|
|
34
|
+
* The session's permission select, folded from the three whole-value
|
|
35
|
+
* knob events (`permission/preset`, `sandbox/mode`, `approval/policy`)
|
|
36
|
+
* over the composition defaults. Key absence means no permission service
|
|
37
|
+
* is composed — clients hide the control.
|
|
38
|
+
*/
|
|
39
|
+
permissions: PermissionSelect;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure types of the permission domain: the ONE home of the `permissions`
|
|
3
|
+
* projection-key declaration plus its payload types, free of this package's
|
|
4
|
+
* host-side value imports (cordis, schemastery). Two namespace projections
|
|
5
|
+
* serve it — the package root re-export for host consumers, `./client` (the
|
|
6
|
+
* browser half-entry's re-export) for client aggregates — with zero content
|
|
7
|
+
* duplication.
|
|
8
|
+
*
|
|
9
|
+
* @module @stackstackstack/dsh-permission-presets/types
|
|
10
|
+
*/
|
|
11
|
+
export {};
|
|
12
|
+
//# sourceMappingURL=types.js.map
|
package/package.json
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@stackstackstack/dsh-permission-presets",
|
|
3
|
+
"description": "User-facing permission presets (ctx.permissionPresets) for the DeepSeek Harness: one product-level Permissions select bundling the sandbox-mode and approval-policy knobs, written through to their own session events",
|
|
4
|
+
"version": "0.1.5",
|
|
5
|
+
"publishConfig": {
|
|
6
|
+
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
|
|
11
|
+
"directory": "packages/interaction/permission-presets"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./invariant": {
|
|
22
|
+
"types": "./lib/types/invariant.d.ts",
|
|
23
|
+
"default": "./lib/invariant.js"
|
|
24
|
+
},
|
|
25
|
+
"./types": {
|
|
26
|
+
"types": "./lib/types/types.d.ts",
|
|
27
|
+
"default": "./lib/types/types.js"
|
|
28
|
+
},
|
|
29
|
+
"./client": {
|
|
30
|
+
"types": "./lib/types/client.d.ts",
|
|
31
|
+
"default": "./lib/types/client.js"
|
|
32
|
+
},
|
|
33
|
+
"./src/*": "./src/*",
|
|
34
|
+
"./package.json": "./package.json"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"lib/index.js",
|
|
38
|
+
"lib/invariant.js",
|
|
39
|
+
"lib/types/**/*.js",
|
|
40
|
+
"lib/types/**/*.d.ts"
|
|
41
|
+
],
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@stackstackstack/dsh-shell": "^0.1.5",
|
|
45
|
+
"@stackstackstack/dsh-commands": "^0.1.5",
|
|
46
|
+
"@stackstackstack/dsh-sandbox": "^0.1.5",
|
|
47
|
+
"@stackstackstack/dsh-sandbox-policy": "^0.1.5",
|
|
48
|
+
"@stackstackstack/dsh-session": "^0.1.5",
|
|
49
|
+
"@stackstackstack/dsh-session-projection": "^0.1.5",
|
|
50
|
+
"@stackstackstack/dsh-settings": "^0.1.5",
|
|
51
|
+
"@stackstackstack/dsh-invariants": "^0.1.5",
|
|
52
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
53
|
+
"@stackstackstack/dsh-user-approval": "^0.1.5"
|
|
54
|
+
},
|
|
55
|
+
"dependencies": {
|
|
56
|
+
"zod": "^4.4.3",
|
|
57
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"@stackstackstack/dsh-shell": "^0.1.5",
|
|
61
|
+
"@stackstackstack/dsh-invariants": "^0.1.5",
|
|
62
|
+
"@stackstackstack/dsh-commands": "^0.1.5",
|
|
63
|
+
"@stackstackstack/dsh-sandbox": "^0.1.5",
|
|
64
|
+
"@stackstackstack/dsh-sandbox-policy": "^0.1.5",
|
|
65
|
+
"@stackstackstack/dsh-session": "^0.1.5",
|
|
66
|
+
"@stackstackstack/dsh-session-projection": "^0.1.5",
|
|
67
|
+
"@stackstackstack/dsh-settings": "^0.1.5",
|
|
68
|
+
"@stackstackstack/dsh-user-approval": "^0.1.5",
|
|
69
|
+
"@deepseek-ai/cordis": "^4.0.1"
|
|
70
|
+
}
|
|
71
|
+
}
|