@exagone313/dsh-podman 0.2.0-rc.2
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.md +41 -0
- package/README.zh.md +41 -0
- package/cordis.patch.yml +17 -0
- package/dist/approval-reasons.d.ts +129 -0
- package/dist/approval-reasons.js +253 -0
- package/dist/approval-reasons.test.d.ts +1 -0
- package/dist/approval-reasons.test.js +115 -0
- package/dist/approval.d.ts +29 -0
- package/dist/approval.js +412 -0
- package/dist/approval.test.d.ts +1 -0
- package/dist/approval.test.js +462 -0
- package/dist/card-route.d.ts +15 -0
- package/dist/card-route.js +478 -0
- package/dist/card-route.test.d.ts +1 -0
- package/dist/card-route.test.js +181 -0
- package/dist/card-test-support.d.ts +15 -0
- package/dist/card-test-support.js +160 -0
- package/dist/client/ContainerCard.d.ts +6 -0
- package/dist/client/ContainerCard.js +79 -0
- package/dist/client/card-client.d.ts +6 -0
- package/dist/client/card-client.js +47 -0
- package/dist/client/card-protocol.d.ts +101 -0
- package/dist/client/card-protocol.js +10 -0
- package/dist/client/container-card-caches.d.ts +10 -0
- package/dist/client/container-card-caches.js +31 -0
- package/dist/client/container-card-controller.d.ts +107 -0
- package/dist/client/container-card-controller.js +207 -0
- package/dist/client/container-card-create-modal.d.ts +24 -0
- package/dist/client/container-card-create-modal.js +109 -0
- package/dist/client/container-card-directory-styles.d.ts +5 -0
- package/dist/client/container-card-directory-styles.js +246 -0
- package/dist/client/container-card-directory.d.ts +12 -0
- package/dist/client/container-card-directory.js +144 -0
- package/dist/client/container-card-editors.d.ts +30 -0
- package/dist/client/container-card-editors.js +116 -0
- package/dist/client/container-card-images.d.ts +37 -0
- package/dist/client/container-card-images.js +35 -0
- package/dist/client/container-card-paths.d.ts +20 -0
- package/dist/client/container-card-paths.js +88 -0
- package/dist/client/container-card-row.d.ts +30 -0
- package/dist/client/container-card-row.js +54 -0
- package/dist/client/container-card-secrets.d.ts +21 -0
- package/dist/client/container-card-secrets.js +71 -0
- package/dist/client/container-card-shared.d.ts +55 -0
- package/dist/client/container-card-shared.js +152 -0
- package/dist/client/container-card-styles.d.ts +21 -0
- package/dist/client/container-card-styles.js +143 -0
- package/dist/client/container-card-volumes.d.ts +12 -0
- package/dist/client/container-card-volumes.js +37 -0
- package/dist/client/container-card-workspace.d.ts +31 -0
- package/dist/client/container-card-workspace.js +24 -0
- package/dist/client/directory-picker.d.ts +12 -0
- package/dist/client/directory-picker.js +30 -0
- package/dist/client/index.d.ts +5 -0
- package/dist/client/index.js +4419 -0
- package/dist/client/locales.d.ts +9 -0
- package/dist/client/locales.js +370 -0
- package/dist/client/read-only-approval.d.ts +8 -0
- package/dist/client/read-only-approval.js +63 -0
- package/dist/client/slot-contract.d.ts +12 -0
- package/dist/client/slot-contract.js +4 -0
- package/dist/client/terminal-styles.d.ts +5 -0
- package/dist/client/terminal-styles.js +31 -0
- package/dist/client/tool-views.d.ts +8 -0
- package/dist/client/tool-views.js +241 -0
- package/dist/containers.test.d.ts +1 -0
- package/dist/containers.test.js +443 -0
- package/dist/daemons.test.d.ts +1 -0
- package/dist/daemons.test.js +153 -0
- package/dist/fs-provider.d.ts +23 -0
- package/dist/fs-provider.js +184 -0
- package/dist/fs-provider.test.d.ts +1 -0
- package/dist/fs-provider.test.js +260 -0
- package/dist/generated/version.d.ts +2 -0
- package/dist/generated/version.js +6 -0
- package/dist/grpc/proto/dshctl/v1/control.proto +133 -0
- package/dist/grpc/proto/dshguest/v1/guest.proto +79 -0
- package/dist/grpc/runtime-client.d.ts +5 -0
- package/dist/grpc/runtime-client.js +43 -0
- package/dist/guest-rpc.d.ts +62 -0
- package/dist/guest-rpc.js +391 -0
- package/dist/images.test.d.ts +1 -0
- package/dist/images.test.js +79 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +140 -0
- package/dist/misc.test.d.ts +1 -0
- package/dist/misc.test.js +141 -0
- package/dist/mount-enums.d.ts +22 -0
- package/dist/mount-enums.js +81 -0
- package/dist/mount-input.d.ts +14 -0
- package/dist/mount-input.js +64 -0
- package/dist/mounts.test.d.ts +1 -0
- package/dist/mounts.test.js +408 -0
- package/dist/output-reader.d.ts +34 -0
- package/dist/output-reader.js +87 -0
- package/dist/paths.test.d.ts +1 -0
- package/dist/paths.test.js +74 -0
- package/dist/preferences.d.ts +6 -0
- package/dist/preferences.js +27 -0
- package/dist/project-path.d.ts +6 -0
- package/dist/project-path.js +73 -0
- package/dist/project-path.test.d.ts +1 -0
- package/dist/project-path.test.js +52 -0
- package/dist/prompts.d.ts +13 -0
- package/dist/prompts.js +149 -0
- package/dist/public.d.ts +4 -0
- package/dist/public.js +63 -0
- package/dist/read-only-shell.d.ts +14 -0
- package/dist/read-only-shell.js +150 -0
- package/dist/read-only-shell.test.d.ts +1 -0
- package/dist/read-only-shell.test.js +127 -0
- package/dist/secrets.test.d.ts +1 -0
- package/dist/secrets.test.js +181 -0
- package/dist/settings-commands.test.d.ts +1 -0
- package/dist/settings-commands.test.js +549 -0
- package/dist/settings-create.test.d.ts +1 -0
- package/dist/settings-create.test.js +394 -0
- package/dist/settings-mounts.test.d.ts +1 -0
- package/dist/settings-mounts.test.js +467 -0
- package/dist/settings-paths.test.d.ts +1 -0
- package/dist/settings-paths.test.js +65 -0
- package/dist/settings-schema.d.ts +14 -0
- package/dist/settings-schema.js +14 -0
- package/dist/settings-workspaces.test.d.ts +1 -0
- package/dist/settings-workspaces.test.js +265 -0
- package/dist/spill-store.d.ts +17 -0
- package/dist/spill-store.js +54 -0
- package/dist/spill-store.test.d.ts +1 -0
- package/dist/spill-store.test.js +59 -0
- package/dist/subprocess.d.ts +9 -0
- package/dist/subprocess.js +369 -0
- package/dist/subprocess.test.d.ts +1 -0
- package/dist/subprocess.test.js +234 -0
- package/dist/test-support.d.ts +328 -0
- package/dist/test-support.js +437 -0
- package/dist/tool-defs.d.ts +26 -0
- package/dist/tool-defs.js +188 -0
- package/dist/tool-handlers.d.ts +2 -0
- package/dist/tool-handlers.js +546 -0
- package/dist/tool-params.d.ts +803 -0
- package/dist/tool-params.js +494 -0
- package/dist/tool-schemas.d.ts +2 -0
- package/dist/tool-schemas.js +5 -0
- package/dist/tool-views.d.ts +2 -0
- package/dist/tool-views.js +56 -0
- package/dist/views.test.d.ts +1 -0
- package/dist/views.test.js +23 -0
- package/dist/volumes.test.d.ts +1 -0
- package/dist/volumes.test.js +40 -0
- package/dist/workspace-binding.d.ts +40 -0
- package/dist/workspace-binding.js +275 -0
- package/dist/workspace-binding.test.d.ts +1 -0
- package/dist/workspace-binding.test.js +337 -0
- package/docs/architecture.md +173 -0
- package/docs/architecture.zh.md +129 -0
- package/docs/configuration.md +160 -0
- package/docs/configuration.zh.md +147 -0
- package/docs/development.md +142 -0
- package/docs/development.zh.md +135 -0
- package/docs/install-dsh-and-dsh-podman.md +165 -0
- package/docs/install-dsh-and-dsh-podman.zh.md +145 -0
- package/docs/usage.md +393 -0
- package/docs/usage.zh.md +315 -0
- package/package.json +119 -0
package/dist/approval.js
ADDED
|
@@ -0,0 +1,412 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Elouan Martinet <exa@elou.world>
|
|
2
|
+
//
|
|
3
|
+
// SPDX-License-Identifier: MIT
|
|
4
|
+
import { approvalPath, currentCwd } from "./guest-rpc.js";
|
|
5
|
+
import { inferMountKind, projectMountDestinationReason } from "./mount-input.js";
|
|
6
|
+
import { defaultMountMode } from "./mount-enums.js";
|
|
7
|
+
import { TOOLS } from "./tool-schemas.js";
|
|
8
|
+
import { renderDenial, renderReason, } from "./approval-reasons.js";
|
|
9
|
+
// Build the mount fact for one mount object, or undefined when it names no
|
|
10
|
+
// source. `readOnly` is the mount's mode when the call carries one; omit it
|
|
11
|
+
// when the call does not (removal), so the reason names no mode.
|
|
12
|
+
function mountFact(mount, readOnly) {
|
|
13
|
+
const inferred = inferMountKind(mount);
|
|
14
|
+
const kind = inferred === "volume" || inferred === "secret" || inferred === "tmpfs"
|
|
15
|
+
? inferred
|
|
16
|
+
: "project";
|
|
17
|
+
const destination = typeof mount.destination === "string" && mount.destination !== ""
|
|
18
|
+
? mount.destination
|
|
19
|
+
: undefined;
|
|
20
|
+
const source = kind === "tmpfs"
|
|
21
|
+
? ""
|
|
22
|
+
: kind === "volume"
|
|
23
|
+
? typeof mount.volume === "string" ? mount.volume : ""
|
|
24
|
+
: kind === "secret"
|
|
25
|
+
? typeof mount.secret === "string" ? mount.secret : ""
|
|
26
|
+
: typeof mount.project === "string"
|
|
27
|
+
? mount.project
|
|
28
|
+
: "";
|
|
29
|
+
if (kind !== "tmpfs" && source === "")
|
|
30
|
+
return undefined;
|
|
31
|
+
return {
|
|
32
|
+
kind,
|
|
33
|
+
source,
|
|
34
|
+
...(destination === undefined ? {} : { destination }),
|
|
35
|
+
...(readOnly === undefined ? {} : { readOnly }),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
// Build the locale-independent reason fact for a gated call, or undefined when
|
|
39
|
+
// the arguments carry too little to describe the call.
|
|
40
|
+
export function reasonFact(name, args, sessionCwd) {
|
|
41
|
+
const str = (key) => typeof args[key] === "string" && args[key] !== "" ? args[key] : undefined;
|
|
42
|
+
const listOf = (key) => {
|
|
43
|
+
const value = args[key];
|
|
44
|
+
return Array.isArray(value) && value.length > 0
|
|
45
|
+
? value.map((item) => String(item))
|
|
46
|
+
: undefined;
|
|
47
|
+
};
|
|
48
|
+
const mountItems = () => {
|
|
49
|
+
const value = args.mounts;
|
|
50
|
+
if (!Array.isArray(value) || value.length === 0)
|
|
51
|
+
return undefined;
|
|
52
|
+
const items = value
|
|
53
|
+
.map((item) => {
|
|
54
|
+
if (typeof item !== "object" || item === null)
|
|
55
|
+
return undefined;
|
|
56
|
+
const mount = item;
|
|
57
|
+
const mode = typeof mount.mode === "string" && mount.mode !== ""
|
|
58
|
+
? mount.mode
|
|
59
|
+
: defaultMountMode(inferMountKind(mount));
|
|
60
|
+
return mountFact(mount, mode !== "read_write");
|
|
61
|
+
})
|
|
62
|
+
.filter((item) => item !== undefined);
|
|
63
|
+
return items.length === 0 ? undefined : items;
|
|
64
|
+
};
|
|
65
|
+
const envKeys = () => {
|
|
66
|
+
const value = args.env;
|
|
67
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
const keys = Object.keys(value);
|
|
71
|
+
return keys.length === 0 ? undefined : keys;
|
|
72
|
+
};
|
|
73
|
+
const numberListOf = (key) => {
|
|
74
|
+
const value = args[key];
|
|
75
|
+
return Array.isArray(value) && value.length > 0
|
|
76
|
+
? value.map((item) => Number(item))
|
|
77
|
+
: undefined;
|
|
78
|
+
};
|
|
79
|
+
// The optional process identity a tool may carry, shared by the tools that
|
|
80
|
+
// can run a process as another user.
|
|
81
|
+
const identityFact = () => {
|
|
82
|
+
const uid = typeof args.uid === "number" ? args.uid : undefined;
|
|
83
|
+
const gid = typeof args.gid === "number" ? args.gid : undefined;
|
|
84
|
+
const groups = numberListOf("groups");
|
|
85
|
+
return {
|
|
86
|
+
...(uid === undefined ? {} : { uid }),
|
|
87
|
+
...(gid === undefined ? {} : { gid }),
|
|
88
|
+
...(groups === undefined ? {} : { groups }),
|
|
89
|
+
};
|
|
90
|
+
};
|
|
91
|
+
switch (name) {
|
|
92
|
+
case "image_build": {
|
|
93
|
+
const image = str("imageId");
|
|
94
|
+
if (image === undefined)
|
|
95
|
+
return undefined;
|
|
96
|
+
const parent = str("parent");
|
|
97
|
+
const packages = listOf("packages");
|
|
98
|
+
return {
|
|
99
|
+
kind: "image_build",
|
|
100
|
+
image,
|
|
101
|
+
...(parent === undefined ? {} : { parent }),
|
|
102
|
+
...(packages === undefined ? {} : { packages }),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
case "image_rebuild": {
|
|
106
|
+
const image = str("imageId");
|
|
107
|
+
return image === undefined ? undefined : { kind: "image_rebuild", image };
|
|
108
|
+
}
|
|
109
|
+
case "image_rebuild_all":
|
|
110
|
+
return { kind: "image_rebuild_all" };
|
|
111
|
+
case "image_remove": {
|
|
112
|
+
const image = str("imageId");
|
|
113
|
+
return image === undefined ? undefined : { kind: "image_remove", image };
|
|
114
|
+
}
|
|
115
|
+
case "container_recreate":
|
|
116
|
+
case "container_start": {
|
|
117
|
+
const container = str("container");
|
|
118
|
+
if (container === undefined)
|
|
119
|
+
return undefined;
|
|
120
|
+
const image = str("image");
|
|
121
|
+
const mounts = mountItems();
|
|
122
|
+
const env = envKeys();
|
|
123
|
+
const paths = listOf("paths");
|
|
124
|
+
return {
|
|
125
|
+
kind: name,
|
|
126
|
+
container,
|
|
127
|
+
...(image === undefined ? {} : { image }),
|
|
128
|
+
...(mounts === undefined ? {} : { mounts }),
|
|
129
|
+
...(env === undefined ? {} : { env }),
|
|
130
|
+
...(paths === undefined ? {} : { paths }),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
case "container_remove": {
|
|
134
|
+
const container = str("container");
|
|
135
|
+
return container === undefined ? undefined : { kind: "container_remove", container };
|
|
136
|
+
}
|
|
137
|
+
case "volume_remove": {
|
|
138
|
+
const volume = str("name");
|
|
139
|
+
return volume === undefined ? undefined : { kind: "volume_remove", name: volume };
|
|
140
|
+
}
|
|
141
|
+
case "secret_remove": {
|
|
142
|
+
const secret = str("name");
|
|
143
|
+
return secret === undefined ? undefined : { kind: "secret_remove", name: secret };
|
|
144
|
+
}
|
|
145
|
+
case "container_secret_add": {
|
|
146
|
+
const container = str("container");
|
|
147
|
+
const env = str("env");
|
|
148
|
+
const secret = str("secret");
|
|
149
|
+
if (container === undefined || env === undefined || secret === undefined) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
return { kind: "container_secret_add", container, secret, env };
|
|
153
|
+
}
|
|
154
|
+
case "container_secret_remove": {
|
|
155
|
+
const container = str("container");
|
|
156
|
+
const env = str("env");
|
|
157
|
+
if (container === undefined || env === undefined)
|
|
158
|
+
return undefined;
|
|
159
|
+
return { kind: "container_secret_remove", container, env };
|
|
160
|
+
}
|
|
161
|
+
case "container_mount_add":
|
|
162
|
+
case "container_mount_remove":
|
|
163
|
+
case "container_mount_update": {
|
|
164
|
+
const container = str("container");
|
|
165
|
+
if (container === undefined)
|
|
166
|
+
return undefined;
|
|
167
|
+
// Removal carries no mode, so its reason names none; add and update name
|
|
168
|
+
// the mode the call asks for (add defaults it like the mount schema).
|
|
169
|
+
const readOnly = name === "container_mount_remove"
|
|
170
|
+
? undefined
|
|
171
|
+
: (typeof args.mode === "string" && args.mode !== ""
|
|
172
|
+
? args.mode
|
|
173
|
+
: defaultMountMode(inferMountKind(args))) !== "read_write";
|
|
174
|
+
const mount = mountFact(args, readOnly);
|
|
175
|
+
if (mount === undefined)
|
|
176
|
+
return undefined;
|
|
177
|
+
return { kind: name, container, mount };
|
|
178
|
+
}
|
|
179
|
+
case "container_path_set": {
|
|
180
|
+
const container = str("container");
|
|
181
|
+
if (container === undefined)
|
|
182
|
+
return undefined;
|
|
183
|
+
const paths = Array.isArray(args.paths)
|
|
184
|
+
? args.paths.map((path) => String(path))
|
|
185
|
+
: [];
|
|
186
|
+
return { kind: "container_path_set", container, paths };
|
|
187
|
+
}
|
|
188
|
+
case "container_path_add":
|
|
189
|
+
case "container_path_remove": {
|
|
190
|
+
const container = str("container");
|
|
191
|
+
const path = str("path");
|
|
192
|
+
if (container === undefined || path === undefined)
|
|
193
|
+
return undefined;
|
|
194
|
+
return { kind: name, container, path };
|
|
195
|
+
}
|
|
196
|
+
case "container_bash": {
|
|
197
|
+
const container = str("container");
|
|
198
|
+
const command = str("command");
|
|
199
|
+
if (container === undefined || command === undefined)
|
|
200
|
+
return undefined;
|
|
201
|
+
const cwd = str("workdir");
|
|
202
|
+
return {
|
|
203
|
+
kind: "container_bash",
|
|
204
|
+
container,
|
|
205
|
+
command,
|
|
206
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
207
|
+
...identityFact(),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
case "container_exec": {
|
|
211
|
+
const container = str("container");
|
|
212
|
+
const argv = listOf("argv");
|
|
213
|
+
if (container === undefined || argv === undefined)
|
|
214
|
+
return undefined;
|
|
215
|
+
const cwd = str("workdir");
|
|
216
|
+
return {
|
|
217
|
+
kind: "container_exec",
|
|
218
|
+
container,
|
|
219
|
+
argv,
|
|
220
|
+
...(cwd === undefined ? {} : { cwd }),
|
|
221
|
+
...identityFact(),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
case "container_write":
|
|
225
|
+
case "container_edit": {
|
|
226
|
+
const container = str("container");
|
|
227
|
+
const path = str("file_path");
|
|
228
|
+
if (container === undefined || path === undefined)
|
|
229
|
+
return undefined;
|
|
230
|
+
return { kind: name, container, path: approvalPath(path, sessionCwd) };
|
|
231
|
+
}
|
|
232
|
+
case "daemon_start": {
|
|
233
|
+
const container = str("container");
|
|
234
|
+
const argv = listOf("argv");
|
|
235
|
+
if (container === undefined || argv === undefined)
|
|
236
|
+
return undefined;
|
|
237
|
+
const daemon = str("name");
|
|
238
|
+
return {
|
|
239
|
+
kind: "daemon_start",
|
|
240
|
+
container,
|
|
241
|
+
argv,
|
|
242
|
+
...(daemon === undefined ? {} : { name: daemon }),
|
|
243
|
+
...identityFact(),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
default:
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Build the single-line approval summary shown for a gated tool call.
|
|
251
|
+
//
|
|
252
|
+
// sessionCwd is used to name the resolved target of a relative path, so the
|
|
253
|
+
// prompt describes the file that will actually be touched. `locale` selects
|
|
254
|
+
// the language; it defaults to English.
|
|
255
|
+
export function summarizeArgs(name, args, sessionCwd, locale = "en") {
|
|
256
|
+
const fact = reasonFact(name, args, sessionCwd);
|
|
257
|
+
return fact === undefined ? "" : renderReason(locale, fact);
|
|
258
|
+
}
|
|
259
|
+
// The ask reason, omitted when no summary can be derived so the approval panel
|
|
260
|
+
// shows its own localized fallback instead of a blank headline (the panel only
|
|
261
|
+
// substitutes for a nullish reason, not an empty string).
|
|
262
|
+
function askReason(name, args, sessionCwd, locale = "en") {
|
|
263
|
+
const reason = summarizeArgs(name, args, sessionCwd, locale);
|
|
264
|
+
return reason === "" ? {} : { reason };
|
|
265
|
+
}
|
|
266
|
+
// Decide whether a tool call needs approval. DSH resolves an `ask` decision
|
|
267
|
+
// through its approval service (`ctx.get("approval").request(...)`), showing
|
|
268
|
+
// the standard approval prompt; without one the call fails closed.
|
|
269
|
+
// Approval is per tool, optionally conditional on the call's arguments via a
|
|
270
|
+
// tool's `approvalWhen` predicate (e.g. container_start only asks when project
|
|
271
|
+
// mounts are supplied).
|
|
272
|
+
export function approvalDecision(name, args, sessionCwd, locale = "en") {
|
|
273
|
+
const tool = TOOLS.find((entry) => entry.name === name);
|
|
274
|
+
if (tool === undefined)
|
|
275
|
+
return undefined;
|
|
276
|
+
if (tool.approval === true) {
|
|
277
|
+
return { kind: "ask", ...askReason(name, args ?? {}, sessionCwd, locale) };
|
|
278
|
+
}
|
|
279
|
+
if (tool.approvalWhen !== undefined && tool.approvalWhen(args ?? {})) {
|
|
280
|
+
return { kind: "ask", ...askReason(name, args ?? {}, sessionCwd, locale) };
|
|
281
|
+
}
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
// Tools that only read or list and remain safe under a read-only permission.
|
|
285
|
+
export const READ_ONLY_TOOLS = new Set([
|
|
286
|
+
"image_list",
|
|
287
|
+
"image_get",
|
|
288
|
+
"container_list",
|
|
289
|
+
"container_read",
|
|
290
|
+
"container_glob",
|
|
291
|
+
"container_grep",
|
|
292
|
+
"container_mount_list",
|
|
293
|
+
"volume_list",
|
|
294
|
+
"secret_list",
|
|
295
|
+
"daemon_list",
|
|
296
|
+
"daemon_logs",
|
|
297
|
+
]);
|
|
298
|
+
// The agent preset id this plugin ships ("Podman operator mode"). Tools in
|
|
299
|
+
// PODMAN_OPS_APPROVAL_TOOLS are gated behind approval only for agents composed
|
|
300
|
+
// from this preset, so it can "do other things" (run commands, edit container
|
|
301
|
+
// files, start daemons) once the user approves.
|
|
302
|
+
export const PODMAN_OPS_PRESET = "podman-ops";
|
|
303
|
+
// Tools the Podman operator-mode preset keeps available but approval-gated.
|
|
304
|
+
export const PODMAN_OPS_APPROVAL_TOOLS = new Set([
|
|
305
|
+
"container_bash",
|
|
306
|
+
"container_exec",
|
|
307
|
+
"container_write",
|
|
308
|
+
"container_edit",
|
|
309
|
+
"daemon_start",
|
|
310
|
+
]);
|
|
311
|
+
// Every tool this plugin registers.
|
|
312
|
+
const OUR_TOOL_NAMES = new Set(TOOLS.map((tool) => tool.name));
|
|
313
|
+
// The harness's built-in tools that touch the container's filesystem or spawn
|
|
314
|
+
// processes in it. The harness fences these with its fs sandbox and its
|
|
315
|
+
// bash/pwsh sandbox executors, but this plugin replaces ctx.fs/ctx.subprocess
|
|
316
|
+
// and unwraps the landlock-run fence, so nothing else enforces read-only for
|
|
317
|
+
// them. Deny them here instead. `pwsh` exists only on Windows; the other
|
|
318
|
+
// built-ins that can reach the container (`subagent`, `workflow`, `ralph`)
|
|
319
|
+
// delegate to child agents that inherit the session's mode, so this policy
|
|
320
|
+
// already covers them.
|
|
321
|
+
const BUILTIN_FILE_TOOLS = new Set(["write", "edit", "bash", "pwsh"]);
|
|
322
|
+
// The `tools/pre-execute` policy, driven by the session's permission knobs:
|
|
323
|
+
// - read-only sandbox: the built-in shell/file tools and their container
|
|
324
|
+
// counterparts run only when the read-only shell gate allows them (every
|
|
325
|
+
// mount is read-only); every other mutating tool is denied with a reason.
|
|
326
|
+
// - approval policy "never" (Full access): run without asking.
|
|
327
|
+
// - otherwise (Workspace Write): ask for the approval-gated tools, including
|
|
328
|
+
// the tools the Podman operator-mode preset gates per preset.
|
|
329
|
+
export async function preExecutePolicy(exec, next, getProjectsRoot, getLocale, readSession, readOnlyShell) {
|
|
330
|
+
const name = exec.name;
|
|
331
|
+
const locale = getLocale?.() ?? "en";
|
|
332
|
+
const session = exec.agent?.session;
|
|
333
|
+
const facts = session === undefined ? {} : readSession?.(session) ?? {};
|
|
334
|
+
if (facts.mode === "read-only" && readOnlyShell !== undefined) {
|
|
335
|
+
const gated = await readOnlyShell(exec, locale);
|
|
336
|
+
// `allow` delegates so the rest of the pre-execute waterfall still runs.
|
|
337
|
+
if (gated?.kind === "allow")
|
|
338
|
+
return next();
|
|
339
|
+
if (gated !== undefined)
|
|
340
|
+
return gated;
|
|
341
|
+
}
|
|
342
|
+
if (facts.mode === "read-only" && BUILTIN_FILE_TOOLS.has(name)) {
|
|
343
|
+
return {
|
|
344
|
+
kind: "deny",
|
|
345
|
+
reason: renderDenial(locale, { kind: "read_only_builtin", tool: name }),
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
if (!OUR_TOOL_NAMES.has(name))
|
|
349
|
+
return next();
|
|
350
|
+
if (facts.mode === "read-only") {
|
|
351
|
+
if (READ_ONLY_TOOLS.has(name))
|
|
352
|
+
return next();
|
|
353
|
+
return {
|
|
354
|
+
kind: "deny",
|
|
355
|
+
reason: renderDenial(locale, { kind: "read_only", tool: name }),
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const args = exec.arguments;
|
|
359
|
+
const parsed = typeof args === "object" && args !== null ? args : undefined;
|
|
360
|
+
const projectsRoot = getProjectsRoot?.();
|
|
361
|
+
const denyReason = mountDestinationsReason(name, parsed, projectsRoot, locale);
|
|
362
|
+
if (denyReason !== undefined) {
|
|
363
|
+
return { kind: "deny", reason: denyReason };
|
|
364
|
+
}
|
|
365
|
+
if (facts.policy === "never")
|
|
366
|
+
return next();
|
|
367
|
+
const sessionCwd = currentCwd(exec);
|
|
368
|
+
if (facts.preset === PODMAN_OPS_PRESET && PODMAN_OPS_APPROVAL_TOOLS.has(name)) {
|
|
369
|
+
return { kind: "ask", ...askReason(name, parsed ?? {}, sessionCwd, locale) };
|
|
370
|
+
}
|
|
371
|
+
return approvalDecision(name, parsed, sessionCwd, locale) ?? next();
|
|
372
|
+
}
|
|
373
|
+
// The reason to deny a mount-bearing tool call that puts a destination on a
|
|
374
|
+
// project (or unnamed) mount, or undefined when the call is allowed. Checks the
|
|
375
|
+
// single-item tools (container_mount_add/remove) and the mounts arrays of
|
|
376
|
+
// container_start/container_recreate.
|
|
377
|
+
export function mountDestinationsReason(name, args, projectsRoot, locale = "en") {
|
|
378
|
+
if (args === undefined)
|
|
379
|
+
return undefined;
|
|
380
|
+
if (name === "container_mount_add" || name === "container_mount_remove" || name === "container_mount_update") {
|
|
381
|
+
return projectMountDestinationReason(projectsRoot, args, locale);
|
|
382
|
+
}
|
|
383
|
+
if (name === "container_start" || name === "container_recreate") {
|
|
384
|
+
const mounts = args.mounts;
|
|
385
|
+
if (!Array.isArray(mounts))
|
|
386
|
+
return undefined;
|
|
387
|
+
for (const item of mounts) {
|
|
388
|
+
if (typeof item !== "object" || item === null)
|
|
389
|
+
continue;
|
|
390
|
+
const reason = projectMountDestinationReason(projectsRoot, item, locale);
|
|
391
|
+
if (reason !== undefined)
|
|
392
|
+
return reason;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
// The harness's sandbox-escalation reason prefix. `approveEscalation` builds it
|
|
398
|
+
// as `escalate sandbox to <mode>: <justification>` in English regardless of the
|
|
399
|
+
// UI language (it is the asker's audit reason, not localized copy), and every
|
|
400
|
+
// escalating tool (bash, pwsh, the fs tools) goes through that one helper.
|
|
401
|
+
export const SANDBOX_ESCALATION_REASON_PREFIX = "escalate sandbox to ";
|
|
402
|
+
// Whether an approval request is the harness asking to widen the sandbox mode
|
|
403
|
+
// for one call. In this deployment the sandbox is bypassed inside the container
|
|
404
|
+
// (the plugin replaces ctx.subprocess/ctx.fs and unwraps the landlock-run
|
|
405
|
+
// fence), so the grant changes nothing and the prompt is noise. Matching the
|
|
406
|
+
// reason rather than the tool name keeps a real approval for the same tool
|
|
407
|
+
// from ever being claimed: if the wording changes, the prompt simply returns.
|
|
408
|
+
export function isSandboxEscalation(request) {
|
|
409
|
+
const reason = request?.reason;
|
|
410
|
+
return (typeof reason === "string" &&
|
|
411
|
+
reason.startsWith(SANDBOX_ESCALATION_REASON_PREFIX));
|
|
412
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|