@opencode-cockpit/trust 0.0.0-stage → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +241 -2
- package/dist/cli/preview.js +251 -0
- package/dist/core/adapt/seen.js +24 -0
- package/dist/core/adapt/v1.js +109 -0
- package/dist/core/adapt/v2.js +109 -0
- package/dist/core/config.js +93 -0
- package/dist/core/danger.js +366 -0
- package/dist/core/engine.js +245 -0
- package/dist/core/family.js +512 -0
- package/dist/core/history.js +169 -0
- package/dist/core/index.js +16 -0
- package/dist/core/keys.js +149 -0
- package/dist/core/ledger.js +202 -0
- package/dist/core/paths.js +38 -0
- package/dist/core/policy.js +128 -0
- package/dist/core/rules.js +152 -0
- package/dist/core/sample.js +311 -0
- package/dist/core/shell.js +253 -0
- package/dist/core/signature.js +38 -0
- package/dist/core/view/actions.js +321 -0
- package/dist/core/view/activity.js +557 -0
- package/dist/core/view/explorer.js +970 -0
- package/dist/core/view/model.js +193 -0
- package/dist/core/view/parts.js +290 -0
- package/dist/core/view/rows.js +165 -0
- package/dist/core/view/sidebar.js +170 -0
- package/dist/tui/index.js +920 -0
- package/dist/tui/journal.js +80 -0
- package/dist/tui/render.js +85 -0
- package/dist/tui/source.js +112 -0
- package/dist/tui/view/dialog.js +43 -0
- package/dist/tui/view/rows.js +69 -0
- package/package.json +61 -4
- package/tui.js +6 -0
- package/types/cli/preview.d.ts +22 -0
- package/types/core/adapt/seen.d.ts +46 -0
- package/types/core/adapt/v1.d.ts +18 -0
- package/types/core/adapt/v2.d.ts +20 -0
- package/types/core/config.d.ts +57 -0
- package/types/core/danger.d.ts +50 -0
- package/types/core/engine.d.ts +117 -0
- package/types/core/family.d.ts +97 -0
- package/types/core/history.d.ts +81 -0
- package/types/core/index.d.ts +16 -0
- package/types/core/keys.d.ts +68 -0
- package/types/core/ledger.d.ts +158 -0
- package/types/core/paths.d.ts +24 -0
- package/types/core/policy.d.ts +52 -0
- package/types/core/rules.d.ts +55 -0
- package/types/core/sample.d.ts +22 -0
- package/types/core/shell.d.ts +40 -0
- package/types/core/signature.d.ts +18 -0
- package/types/core/view/actions.d.ts +76 -0
- package/types/core/view/activity.d.ts +101 -0
- package/types/core/view/explorer.d.ts +154 -0
- package/types/core/view/model.d.ts +106 -0
- package/types/core/view/parts.d.ts +76 -0
- package/types/core/view/rows.d.ts +60 -0
- package/types/core/view/sidebar.d.ts +62 -0
- package/types/tui/index.d.ts +15 -0
- package/types/tui/journal.d.ts +19 -0
- package/types/tui/render.d.ts +14 -0
- package/types/tui/source.d.ts +35 -0
- package/types/tui/view/dialog.d.ts +24 -0
- package/types/tui/view/rows.d.ts +19 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What `x`, `w` and `c` do, on whatever is selected — a command, a family, an answer in the feed, or
|
|
3
|
+
* OpenCode's own "always". Pure: each returns the events to append and the sentence to say. Nothing
|
|
4
|
+
* in the ledger is ever changed in place.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { anyOf, readSubject, showSubject, widenable } from "../family.js";
|
|
8
|
+
import { leadOf } from "./model.js";
|
|
9
|
+
import { agentsText, plural } from "./parts.js";
|
|
10
|
+
/** What a widening never covers, in words — the three `family.outside` holds back. */
|
|
11
|
+
export const EXCEPT = "except dangerous ones, ones that write a file and ones that run another program";
|
|
12
|
+
export const NOT_COVERED = "dangerous ones, and any that write a file or run another program";
|
|
13
|
+
const revoked = (permission, agent, subject) => ({
|
|
14
|
+
v: 1,
|
|
15
|
+
at: 0,
|
|
16
|
+
type: "revoked",
|
|
17
|
+
permission,
|
|
18
|
+
agent,
|
|
19
|
+
subject
|
|
20
|
+
});
|
|
21
|
+
const stamp = (events, at) => events.map(event => ({
|
|
22
|
+
...event,
|
|
23
|
+
at
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
/* ─── x ──────────────────────────────────────────────────────────────────────────────────────── */
|
|
27
|
+
|
|
28
|
+
/** What `x` is called on a target: revoke what answers, forget what is only counting. */
|
|
29
|
+
export function revokeLabel(target) {
|
|
30
|
+
if (target.kind === "always") return {
|
|
31
|
+
label: "Revoke",
|
|
32
|
+
off: true
|
|
33
|
+
};
|
|
34
|
+
if (target.kind === "answer") return {
|
|
35
|
+
label: "Revoke",
|
|
36
|
+
off: false
|
|
37
|
+
};
|
|
38
|
+
if (target.kind === "family") {
|
|
39
|
+
const answering = target.family.widened.length > 0 || target.family.commands.some(c => c.phase === "answering");
|
|
40
|
+
return {
|
|
41
|
+
label: answering ? "Revoke all" : "Forget all",
|
|
42
|
+
off: false
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
label: target.command.phase === "answering" ? "Revoke" : "Forget",
|
|
47
|
+
off: false
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* `x`. A command loses its standing for every agent. A family loses every command in it and its
|
|
53
|
+
* widenings. An answer in the feed stops what gave it: the rule's own count, or the widening.
|
|
54
|
+
* OpenCode's own "always" is OpenCode's: Trust cannot take it back, and says so.
|
|
55
|
+
*/
|
|
56
|
+
export function revoke(target, reading, at) {
|
|
57
|
+
const {
|
|
58
|
+
threshold
|
|
59
|
+
} = reading.settings;
|
|
60
|
+
if (target.kind === "always") return {
|
|
61
|
+
events: [],
|
|
62
|
+
notice: {
|
|
63
|
+
text: 'OpenCode keeps its own "always" until it restarts — Trust cannot take it back.',
|
|
64
|
+
tone: "warning"
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
if (target.kind === "answer") {
|
|
68
|
+
const {
|
|
69
|
+
answer
|
|
70
|
+
} = target;
|
|
71
|
+
const events = [];
|
|
72
|
+
const widened = new Set();
|
|
73
|
+
for (const item of answer.items) {
|
|
74
|
+
if (item.via !== undefined) {
|
|
75
|
+
if (widened.has(item.via)) continue;
|
|
76
|
+
widened.add(item.via);
|
|
77
|
+
events.push({
|
|
78
|
+
v: 1,
|
|
79
|
+
at,
|
|
80
|
+
type: "unwidened",
|
|
81
|
+
permission: answer.permission,
|
|
82
|
+
agent: answer.agent,
|
|
83
|
+
family: item.via
|
|
84
|
+
});
|
|
85
|
+
} else events.push(revoked(answer.permission, answer.agent, item.subject));
|
|
86
|
+
}
|
|
87
|
+
const name = answer.items.map(item => showSubject(answer.permission, item.subject)).join(" && ");
|
|
88
|
+
return {
|
|
89
|
+
events: stamp(events, at),
|
|
90
|
+
notice: {
|
|
91
|
+
text: widened.size > 0 ? `Stopped: ${[...widened].map(family => anyOf(answer.permission, family)).join(", ")} no longer answered for ${answer.agent}.` : `Revoked: ${name} is asked again until you approve it ${threshold}× more, as ${answer.agent}.`,
|
|
92
|
+
tone: "muted"
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (target.kind === "command") {
|
|
97
|
+
const {
|
|
98
|
+
command
|
|
99
|
+
} = target;
|
|
100
|
+
const name = showSubject(command.permission, command.subject);
|
|
101
|
+
const agents = command.standings.map(each => each.entry.agent);
|
|
102
|
+
const via = command.standings.find(each => each.stand.kind === "widened");
|
|
103
|
+
const events = command.standings.map(each => revoked(each.entry.permission, each.entry.agent, each.entry.subject));
|
|
104
|
+
const others = agents.length > 1 ? ` — for ${agentsText(agents)}` : "";
|
|
105
|
+
return {
|
|
106
|
+
events: stamp(events, at),
|
|
107
|
+
notice: {
|
|
108
|
+
text: via?.stand.kind === "widened" ? `Forgot ${name}'s own count${others}; ${anyOf(command.permission, via.stand.family)} still answers it — [w] undoes that.` : command.phase === "answering" ? `Revoked: ${name} is asked again until you approve it ${threshold}× more${others}.` : `Forgot the count for ${name}${others}: it starts again from 0.`,
|
|
109
|
+
tone: "muted"
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const {
|
|
114
|
+
family
|
|
115
|
+
} = target;
|
|
116
|
+
const events = [...family.commands.flatMap(command => command.standings.map(each => revoked(each.entry.permission, each.entry.agent, each.entry.subject))), ...family.widened.map(each => ({
|
|
117
|
+
v: 1,
|
|
118
|
+
at,
|
|
119
|
+
type: "unwidened",
|
|
120
|
+
permission: each.permission,
|
|
121
|
+
agent: each.agent,
|
|
122
|
+
family: each.family
|
|
123
|
+
}))];
|
|
124
|
+
const name = showSubject(family.permission, family.family);
|
|
125
|
+
const answering = family.commands.filter(command => command.phase === "answering").length;
|
|
126
|
+
return {
|
|
127
|
+
events: stamp(events, at),
|
|
128
|
+
notice: {
|
|
129
|
+
text: answering > 0 || family.widened.length > 0 ? `Revoked ${plural(family.commands.length, "command")} in ${name}${family.widened.length > 0 ? ", and its widening" : ""} — each is asked again until approved ${threshold}× in a row.` : `Forgot ${plural(family.commands.length, "count")} in ${name}: each starts again from 0.`,
|
|
130
|
+
tone: "muted"
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/* ─── w ──────────────────────────────────────────────────────────────────────────────────────── */
|
|
136
|
+
|
|
137
|
+
/** The family `w` acts on, and for which agent. */
|
|
138
|
+
|
|
139
|
+
export function widenScope(target, families) {
|
|
140
|
+
if (target.kind === "always") return undefined;
|
|
141
|
+
const find = (permission, family) => families.find(each => each.permission === permission && each.family === family);
|
|
142
|
+
if (target.kind === "family") {
|
|
143
|
+
const {
|
|
144
|
+
family
|
|
145
|
+
} = target;
|
|
146
|
+
return {
|
|
147
|
+
permission: family.permission,
|
|
148
|
+
family: family.family,
|
|
149
|
+
...(family.commands[0] ? {
|
|
150
|
+
agent: leadOf(family.commands[0]).entry.agent
|
|
151
|
+
} : {}),
|
|
152
|
+
undo: family.widened.map(each => each.agent)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
const [permission, familyName, agent] = target.kind === "answer" ? [target.answer.permission, target.answer.items.find(item => item.via !== undefined)?.via ?? familyOfAnswer(target.answer, families), target.answer.agent] : [target.command.permission, target.command.family, leadOf(target.command).entry.agent];
|
|
156
|
+
if (familyName === undefined) return undefined;
|
|
157
|
+
const widened = find(permission, familyName)?.widened ?? [];
|
|
158
|
+
return {
|
|
159
|
+
permission,
|
|
160
|
+
family: familyName,
|
|
161
|
+
agent,
|
|
162
|
+
undo: widened.filter(each => each.agent === agent).map(each => each.agent)
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** The family of an answer's first command, as the model grouped it. */
|
|
167
|
+
function familyOfAnswer(answer, families) {
|
|
168
|
+
const first = answer.items[0]?.subject;
|
|
169
|
+
if (first === undefined) return undefined;
|
|
170
|
+
return families.find(family => family.commands.some(command => command.permission === answer.permission && command.subject === first))?.family;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The family's name in a button: commands stay lowercase, where it runs is left to the card. */
|
|
174
|
+
export function familyLabel(permission, family) {
|
|
175
|
+
const text = permission === "bash" ? showSubject("bash", family).replace(/^\(in .*?\) /, "") : family;
|
|
176
|
+
return text.length > 20 ? `${text.slice(0, 19)}…` : text;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** What `w` is called: `Trust any head`, `Undo any head`, or dimmed when the family never widens. */
|
|
180
|
+
export function widenLabel(scope, named = true) {
|
|
181
|
+
if (!scope) return {
|
|
182
|
+
label: "Trust any",
|
|
183
|
+
off: true
|
|
184
|
+
};
|
|
185
|
+
const name = named ? ` ${familyLabel(scope.permission, scope.family)}` : "";
|
|
186
|
+
if (scope.undo.length > 0) return {
|
|
187
|
+
label: `Undo any${name}`,
|
|
188
|
+
off: false
|
|
189
|
+
};
|
|
190
|
+
return {
|
|
191
|
+
label: `Trust any${name}`,
|
|
192
|
+
off: !widenable(scope.permission, scope.family).ok
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* `w`: trust a whole family for one agent, or take that back. Never automatic — this is the only place
|
|
198
|
+
* a `widened` event is made, and only a person reaches it. A dangerous family is refused, with why.
|
|
199
|
+
*/
|
|
200
|
+
export function widen(scope, families, at) {
|
|
201
|
+
if (!scope) return {
|
|
202
|
+
events: [],
|
|
203
|
+
notice: {
|
|
204
|
+
text: "OpenCode's own approvals have no family to widen.",
|
|
205
|
+
tone: "warning"
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
const any = anyOf(scope.permission, scope.family);
|
|
209
|
+
if (scope.undo.length > 0) return {
|
|
210
|
+
events: scope.undo.map(agent => ({
|
|
211
|
+
v: 1,
|
|
212
|
+
at,
|
|
213
|
+
type: "unwidened",
|
|
214
|
+
permission: scope.permission,
|
|
215
|
+
agent,
|
|
216
|
+
family: scope.family
|
|
217
|
+
})),
|
|
218
|
+
notice: {
|
|
219
|
+
text: `Back to exact rules: ${any} is no longer answered for ${agentsText(scope.undo)}.`,
|
|
220
|
+
tone: "muted"
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
const can = widenable(scope.permission, scope.family);
|
|
224
|
+
if (!can.ok) return {
|
|
225
|
+
events: [],
|
|
226
|
+
notice: {
|
|
227
|
+
text: `Not widened: ${can.why}.`,
|
|
228
|
+
tone: "warning"
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
if (scope.agent === undefined) return {
|
|
232
|
+
events: [],
|
|
233
|
+
notice: {
|
|
234
|
+
text: "No agent to widen it for.",
|
|
235
|
+
tone: "warning"
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
const family = families.find(each => each.permission === scope.permission && each.family === scope.family);
|
|
239
|
+
const others = [...new Set((family?.commands ?? []).flatMap(command => command.standings.map(each => each.entry.agent).filter(agent => agent !== scope.agent)))];
|
|
240
|
+
return {
|
|
241
|
+
events: [{
|
|
242
|
+
v: 1,
|
|
243
|
+
at,
|
|
244
|
+
type: "widened",
|
|
245
|
+
permission: scope.permission,
|
|
246
|
+
agent: scope.agent,
|
|
247
|
+
family: scope.family
|
|
248
|
+
}],
|
|
249
|
+
notice: {
|
|
250
|
+
text: `Trusted ${any} for ${scope.agent}${scope.permission === "bash" ? ` — ${EXCEPT} still ask` : ""}.${others.length > 0 ? ` ${agentsText(others)} keep${others.length === 1 ? "s" : ""} counting.` : ""} [w] again undoes it.`,
|
|
251
|
+
tone: "success"
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/* ─── c ──────────────────────────────────────────────────────────────────────────────────────── */
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* The target as `opencode.json` would say it, for you to paste — Trust never writes OpenCode's
|
|
260
|
+
* config. A family is a wildcard (`"ls *": "allow"`), and config says less than a widening: not which
|
|
261
|
+
* agent, and not the commands a widening still asks about.
|
|
262
|
+
*/
|
|
263
|
+
export function configSnippet(target) {
|
|
264
|
+
if (target.kind === "always") {
|
|
265
|
+
const permission = {};
|
|
266
|
+
for (const group of target.groups) for (const pattern of group.patterns) {
|
|
267
|
+
const rules = permission[group.permission] ?? {};
|
|
268
|
+
rules[pattern] = "allow";
|
|
269
|
+
permission[group.permission] = rules;
|
|
270
|
+
}
|
|
271
|
+
return {
|
|
272
|
+
text: JSON.stringify({
|
|
273
|
+
permission
|
|
274
|
+
})
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
if (target.kind === "family") return familySnippet(target.family);
|
|
278
|
+
const [permission, subject] = target.kind === "answer" ? [target.answer.permission, target.answer.items[0]?.subject ?? ""] : [target.command.permission, target.command.subject];
|
|
279
|
+
const placed = subject.match(/^\(in [^)]*\) (.*)$/s);
|
|
280
|
+
const pattern = permission === "webfetch" ? `https://${subject}/*` : placed ? placed[1] : subject;
|
|
281
|
+
const text = JSON.stringify({
|
|
282
|
+
permission: {
|
|
283
|
+
[permission]: {
|
|
284
|
+
[pattern]: "allow"
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
return placed ? {
|
|
289
|
+
text,
|
|
290
|
+
note: "a config rule cannot say where a command runs: this one allows it anywhere"
|
|
291
|
+
} : {
|
|
292
|
+
text
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function familySnippet(family) {
|
|
296
|
+
const notes = [];
|
|
297
|
+
let pattern = family.family;
|
|
298
|
+
if (family.permission === "bash") {
|
|
299
|
+
const read = readSubject(family.family);
|
|
300
|
+
if (read) {
|
|
301
|
+
const env = read.command.env.map(word => word.replace(/=…$/, "=*"));
|
|
302
|
+
pattern = `${[...env, ...read.command.argv].join(" ")} *`;
|
|
303
|
+
if (read.place !== undefined) notes.push("anywhere, not only where it ran");
|
|
304
|
+
}
|
|
305
|
+
} else if (family.permission === "edit") {
|
|
306
|
+
pattern = `${family.family === "./" ? "" : family.family}*`;
|
|
307
|
+
notes.push("its * reaches into subfolders too");
|
|
308
|
+
} else if (family.permission === "webfetch") pattern = `https://${family.family}/*`;
|
|
309
|
+
const any = anyOf(family.permission, family.family);
|
|
310
|
+
notes.unshift(family.widened.length > 0 ? `config cannot say which agent, so it allows ${any} for every one — even the ones Trust still asks about` : `wider than anything Trust answers here: ${any}, for every agent`);
|
|
311
|
+
return {
|
|
312
|
+
text: JSON.stringify({
|
|
313
|
+
permission: {
|
|
314
|
+
[family.permission]: {
|
|
315
|
+
[pattern]: "allow"
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}),
|
|
319
|
+
note: notes.join("; ")
|
|
320
|
+
};
|
|
321
|
+
}
|