@vincemakes/kiso-code 0.45.2 → 0.46.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/README.md +2 -1
- package/dist/builtin.d.ts +5 -4
- package/dist/builtin.js +12 -7
- package/dist/chat.js +121 -19
- package/dist/child-result.d.ts +23 -0
- package/dist/child-result.js +59 -0
- package/dist/config.d.ts +13 -6
- package/dist/config.js +30 -16
- package/dist/create-coding-agent.js +3 -1
- package/dist/dispatch.d.ts +2 -0
- package/dist/dispatch.js +163 -31
- package/dist/empty-sessions.d.ts +4 -2
- package/dist/empty-sessions.js +6 -2
- package/dist/external-editor.js +4 -1
- package/dist/floor.js +71 -26
- package/dist/index.js +89 -29
- package/dist/launch.d.ts +29 -0
- package/dist/launch.js +43 -0
- package/dist/mode.d.ts +90 -17
- package/dist/mode.js +135 -32
- package/dist/protected-shell.js +60 -19
- package/dist/protected-writes.d.ts +2 -2
- package/dist/protected-writes.js +2 -2
- package/dist/readonly-shell.js +16 -14
- package/dist/replay.js +3 -0
- package/dist/resume.js +2 -2
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +41 -11
- package/dist/shell-words.d.ts +34 -1
- package/dist/shell-words.js +84 -4
- package/dist/state.d.ts +31 -0
- package/dist/state.js +88 -2
- package/dist/task-notice.d.ts +22 -0
- package/dist/task-notice.js +86 -0
- package/dist/trust-ui.js +6 -6
- package/package.json +14 -14
package/dist/mode.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Modes — the
|
|
2
|
+
* Modes — the built-in approval tiers, built ON the E1 policy chain
|
|
3
3
|
* (the kernel is untouched). Each tier is an in-process "mode:<name>"
|
|
4
4
|
* extension whose decide() is live — it only speaks when it is the
|
|
5
5
|
* CURRENT tier (otherwise abstain = no opinion, ADR-0042), so /mode
|
|
@@ -7,18 +7,52 @@
|
|
|
7
7
|
* field: an automated denial records decidedBy: "mode:<name>" — the
|
|
8
8
|
* audit sell. User-level extensions stay on the chain AFTER the mode
|
|
9
9
|
* tiers; a user deny always wins (the chain's deny>allow>ask
|
|
10
|
-
* monotonicity —
|
|
10
|
+
* monotonicity — full-access cannot override an extension deny).
|
|
11
|
+
*
|
|
12
|
+
* Two questions, two settings (the modes round, 2026-09-30). The TIER
|
|
13
|
+
* answers what the agent may do. The DON'T-ASK SWITCH answers whether
|
|
14
|
+
* kiso may stop for a person, and it composes with every tier: it never
|
|
15
|
+
* grants anything — what would ask is refused, a model's question is not
|
|
16
|
+
* offered, an uncertain execution stays uncertain. The old `dontAsk` tier
|
|
17
|
+
* was that switch welded to default's authority; it is still accepted,
|
|
18
|
+
* and means exactly that.
|
|
11
19
|
*/
|
|
12
20
|
import { isProtectedWrite } from "./protected-writes.js";
|
|
13
|
-
/** Every tier kiso
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
/** Every tier kiso decides with. */
|
|
22
|
+
export const MODES = ["manual", "default", "accept-edits", "plan", "full-access"];
|
|
23
|
+
/** The tiers kiso OFFERS a person: the picker, shift+tab, the printed
|
|
24
|
+
* list, the help. `manual` left the offer at 0.40.0 — a saved allow
|
|
25
|
+
* outranks its ask, so "asks for every tool" was never quite what it
|
|
26
|
+
* did — and stays accepted, so no config that names it breaks. */
|
|
27
|
+
export const OFFERED_MODES = ["default", "accept-edits", "plan", "full-access"];
|
|
28
|
+
/** What a person reads for each tier — the status row, the picker, the
|
|
29
|
+
* notices. The typed value (config, --mode, KISO_MODE) keeps its hyphen. */
|
|
30
|
+
export const MODE_LABEL = {
|
|
31
|
+
manual: "manual",
|
|
32
|
+
default: "default",
|
|
33
|
+
"accept-edits": "accept edits",
|
|
34
|
+
plan: "plan",
|
|
35
|
+
"full-access": "full access",
|
|
36
|
+
};
|
|
37
|
+
/** Every spelling kiso ever accepted keeps its meaning. `bypass` is
|
|
38
|
+
* full-access's old name. `dontAsk` was a tier: default's decisions with
|
|
39
|
+
* the switch on — so it still means that, and never full-access with the
|
|
40
|
+
* switch on (read that way, an old config would gain authority nobody
|
|
41
|
+
* gave it). */
|
|
42
|
+
const OLD_NAMES = new Map([
|
|
43
|
+
["bypass", { mode: "full-access" }],
|
|
44
|
+
["dontAsk", { mode: "default", dontAsk: true }],
|
|
45
|
+
]);
|
|
46
|
+
/** Every value a config, KISO_MODE, --mode or `/mode <name>` accepts. */
|
|
47
|
+
export const MODE_VALUES = [...MODES, ...OLD_NAMES.keys()];
|
|
48
|
+
/** A typed tier: its value, its label ("full access"), or an old name. */
|
|
49
|
+
export function parseMode(raw) {
|
|
50
|
+
const s = raw.trim();
|
|
51
|
+
const m = MODES.find((x) => x === s || MODE_LABEL[x] === s);
|
|
52
|
+
if (m !== undefined)
|
|
53
|
+
return { mode: m };
|
|
54
|
+
return OLD_NAMES.get(s);
|
|
55
|
+
}
|
|
22
56
|
/** DC-36 — one line per tier, for the picker, TRANSCRIBED FROM decide()
|
|
23
57
|
* below rather than written fresh. A description that drifts from the
|
|
24
58
|
* behaviour is worse than none: this is the row a human reads before
|
|
@@ -41,25 +75,95 @@ export const MODE_NOTE = {
|
|
|
41
75
|
default: "read-only runs; the rest asks — a saved allow still allows",
|
|
42
76
|
"accept-edits": "read-only, edits run; rest asks — a saved allow still allows",
|
|
43
77
|
plan: "reads run; all else is denied — read-only, and a deny wins",
|
|
44
|
-
|
|
45
|
-
dontAsk: "asks nothing: an ask is denied — a saved allow still allows",
|
|
78
|
+
"full-access": "runs without asking — a user deny and the floor still win",
|
|
46
79
|
};
|
|
80
|
+
/** The switch in one line, for a surface that describes it, under the
|
|
81
|
+
* same 60-column rule. The /mode panel does not offer it: don't ask is
|
|
82
|
+
* not a tier, so it never sits beside them (owner, 2026-09-30) — the
|
|
83
|
+
* panel's header names it when it is on. */
|
|
84
|
+
export const DONT_ASK_NOTE = "never asks: what would ask is refused and the run goes on";
|
|
47
85
|
/** The read-only tool set (plan): reading is allowed, everything else
|
|
48
86
|
* denied with the guiding reason. */
|
|
49
87
|
const READ_TOOLS = new Set(["read_file", "list_dir", "search_text", "read_skill"]);
|
|
88
|
+
/** ADR-0058 (3b, D2): stopping a task this session started asks no one —
|
|
89
|
+
* stopping is the safe direction — in every tier but manual. */
|
|
90
|
+
const STOP_TOOLS = new Set(["task_stop"]);
|
|
50
91
|
let current = "default";
|
|
92
|
+
let dontAsk = "off";
|
|
51
93
|
export function getMode() {
|
|
52
94
|
return current;
|
|
53
95
|
}
|
|
96
|
+
/** A tier switch. The switch stays as it is — unless it came with the
|
|
97
|
+
* old dontAsk tier, which this switch leaves. */
|
|
54
98
|
export function setMode(m) {
|
|
55
99
|
current = m;
|
|
100
|
+
if (dontAsk === "old-name")
|
|
101
|
+
dontAsk = "off";
|
|
102
|
+
}
|
|
103
|
+
/** A typed tier, old names included: `/mode dontAsk` is default with the
|
|
104
|
+
* switch on, exactly the tier it used to be. */
|
|
105
|
+
export function applyModeSetting(s) {
|
|
106
|
+
setMode(s.mode);
|
|
107
|
+
if (s.dontAsk === true && dontAsk === "off")
|
|
108
|
+
dontAsk = "old-name";
|
|
109
|
+
}
|
|
110
|
+
/** Whether kiso may stop for a person — every ask endpoint reads this. */
|
|
111
|
+
export function getDontAsk() {
|
|
112
|
+
return dontAsk !== "off";
|
|
113
|
+
}
|
|
114
|
+
export function setDontAsk(on) {
|
|
115
|
+
dontAsk = on ? "on" : "off";
|
|
116
|
+
}
|
|
117
|
+
/** The tier as the status row and the notices name it, with the switch
|
|
118
|
+
* beside it when it is on. */
|
|
119
|
+
export function modeDisplay() {
|
|
120
|
+
return `${current === "plan" ? "plan (read-only)" : MODE_LABEL[current]}${dontAsk !== "off" ? " · don't ask" : ""}`;
|
|
121
|
+
}
|
|
122
|
+
/** KISO_MODE and KISO_DONT_ASK. A value kiso cannot read is no value —
|
|
123
|
+
* the next layer decides, as an unknown KISO_MODE always did. */
|
|
124
|
+
export function envModeLayer(env = process.env) {
|
|
125
|
+
const raw = env.KISO_DONT_ASK?.trim().toLowerCase();
|
|
126
|
+
const on = raw === "1" || raw === "true" || raw === "on" ? true : raw === "0" || raw === "false" || raw === "off" ? false : undefined;
|
|
127
|
+
return { ...(env.KISO_MODE !== undefined ? { mode: env.KISO_MODE } : {}), ...(on !== undefined ? { dontAsk: on } : {}) };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Precedence: flag > env > project config > user config > default, for
|
|
131
|
+
* each answer on its own. The tier comes from the highest layer that
|
|
132
|
+
* names a readable one. The switch comes from the highest layer that
|
|
133
|
+
* either sets it (`--dont-ask`, KISO_DONT_ASK, "dontAsk": true|false) or
|
|
134
|
+
* WON THE TIER with the old name `dontAsk` — an old name's switch counts
|
|
135
|
+
* only where that old name decided the tier. So every combination of old
|
|
136
|
+
* spellings resolves as it did when dontAsk was a tier: a user config
|
|
137
|
+
* saying "dontAsk" under `--mode bypass` still asks, because --mode won.
|
|
138
|
+
*/
|
|
139
|
+
export function resolveModeLayers(layers) {
|
|
140
|
+
const order = [
|
|
141
|
+
["flag", layers.flag],
|
|
142
|
+
["env", layers.env],
|
|
143
|
+
["project", layers.project],
|
|
144
|
+
["user", layers.user],
|
|
145
|
+
];
|
|
146
|
+
let tier = { setting: { mode: "default" }, from: "default" };
|
|
147
|
+
for (const [from, layer] of order) {
|
|
148
|
+
const setting = layer?.mode === undefined ? undefined : parseMode(layer.mode);
|
|
149
|
+
if (setting !== undefined) {
|
|
150
|
+
tier = { setting, from };
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const [from, layer] of order) {
|
|
155
|
+
if (layer?.dontAsk !== undefined)
|
|
156
|
+
return { mode: tier.setting.mode, dontAsk: layer.dontAsk ? "on" : "off", from: { mode: tier.from, dontAsk: from } };
|
|
157
|
+
if (from === tier.from && tier.setting.dontAsk === true)
|
|
158
|
+
return { mode: tier.setting.mode, dontAsk: "old-name", from: { mode: tier.from, dontAsk: from } };
|
|
159
|
+
}
|
|
160
|
+
return { mode: tier.setting.mode, dontAsk: "off", from: { mode: tier.from, dontAsk: "default" } };
|
|
56
161
|
}
|
|
57
|
-
/**
|
|
58
|
-
* config
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return MODES.find((x) => x === raw);
|
|
162
|
+
/** Put a resolved state in force — startup, and again once the project
|
|
163
|
+
* config is known. */
|
|
164
|
+
export function applyModeState(s) {
|
|
165
|
+
current = s.mode;
|
|
166
|
+
dontAsk = s.dontAsk;
|
|
63
167
|
}
|
|
64
168
|
/** The per-tier verdict for a tool call — only when this tier is current. */
|
|
65
169
|
function tierVerdict(tier, call, workspaceRoot) {
|
|
@@ -68,14 +172,13 @@ function tierVerdict(tier, call, workspaceRoot) {
|
|
|
68
172
|
switch (tier) {
|
|
69
173
|
case "manual":
|
|
70
174
|
return { action: "ask" }; // every tool asks
|
|
71
|
-
//
|
|
72
|
-
// what happens to the chain's final ASK: the CLI denies it at the
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
175
|
+
// The don't-ask switch changes no tier's decision. What it changes is
|
|
176
|
+
// what happens to the chain's final ASK: the CLI denies it at the ask
|
|
177
|
+
// endpoint instead of asking (chat.ts). Not a tier deny — that would
|
|
178
|
+
// outrank every allow, and a saved allow and the read-only shell allow
|
|
179
|
+
// must still allow.
|
|
76
180
|
case "default":
|
|
77
|
-
|
|
78
|
-
if (READ_TOOLS.has(call.name))
|
|
181
|
+
if (READ_TOOLS.has(call.name) || STOP_TOOLS.has(call.name))
|
|
79
182
|
return { action: "allow" };
|
|
80
183
|
if (call.name === "write_file" || call.name === "edit_file" || call.name === "shell")
|
|
81
184
|
return { action: "ask" };
|
|
@@ -86,7 +189,7 @@ function tierVerdict(tier, call, workspaceRoot) {
|
|
|
86
189
|
// finding: "allow"-as-no-opinion auto-approved it).
|
|
87
190
|
return { action: "abstain" };
|
|
88
191
|
case "accept-edits":
|
|
89
|
-
if (READ_TOOLS.has(call.name))
|
|
192
|
+
if (READ_TOOLS.has(call.name) || STOP_TOOLS.has(call.name))
|
|
90
193
|
return { action: "allow" };
|
|
91
194
|
// 0.40.0: a write into .git/ or .kiso/ asks even here — both hold
|
|
92
195
|
// configuration that runs (protected-writes.ts)
|
|
@@ -96,22 +199,22 @@ function tierVerdict(tier, call, workspaceRoot) {
|
|
|
96
199
|
return { action: "ask" };
|
|
97
200
|
return { action: "abstain" }; // see "default"
|
|
98
201
|
case "plan":
|
|
99
|
-
if (READ_TOOLS.has(call.name))
|
|
202
|
+
if (READ_TOOLS.has(call.name) || STOP_TOOLS.has(call.name))
|
|
100
203
|
return { action: "allow" };
|
|
101
204
|
return { action: "deny", reason: "plan mode: read-only" };
|
|
102
|
-
case "
|
|
205
|
+
case "full-access":
|
|
103
206
|
return { action: "allow" }; // everything — a REAL allow, never an abstain
|
|
104
207
|
}
|
|
105
208
|
}
|
|
106
|
-
/** The
|
|
209
|
+
/** The built-in mode tiers as chain extensions — named "mode:<tier>"
|
|
107
210
|
* so the runtime's decidedBy records exactly that (the runtime derives
|
|
108
211
|
* approvalPolicies from extensions[].approvals, tagging each with the
|
|
109
212
|
* extension name). The CURRENT tier is first: an all-allow chain records
|
|
110
213
|
* decidedBy = the FIRST SPEAKER, so an auto-allow under the startup mode
|
|
111
214
|
* names that mode honestly. Order never affects verdicts — the chain is
|
|
112
215
|
* deny>allow>ask over the SPEAKING verdicts (abstain = no opinion), so a
|
|
113
|
-
* user extension's deny wins over any mode tier,
|
|
114
|
-
* monotonicity e2e pins it). */
|
|
216
|
+
* user extension's deny wins over any mode tier, full-access included
|
|
217
|
+
* (the monotonicity e2e pins it). */
|
|
115
218
|
export function modeExtensions(workspaceRoot = () => process.cwd()) {
|
|
116
219
|
return [...MODES.filter((m) => m === current), ...MODES.filter((m) => m !== current)].map((m) => ({
|
|
117
220
|
name: `mode:${m}`,
|
package/dist/protected-shell.js
CHANGED
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
* guarantee is the sandbox, which stays post-launch.
|
|
36
36
|
*/
|
|
37
37
|
import { readdirSync, realpathSync, statSync } from "node:fs";
|
|
38
|
-
import { basename
|
|
38
|
+
import { basename } from "node:path";
|
|
39
39
|
import { protectedIdentity, PROTECTED_REFUSAL } from "@vincemakes/kiso-tools-node";
|
|
40
40
|
import { innerLines, unwrap } from "./floor.js";
|
|
41
|
-
import { parseShellLooseChecked } from "./shell-words.js";
|
|
41
|
+
import { hostDialect, msysPath, parseShellLooseChecked, resolveShellPath } from "./shell-words.js";
|
|
42
42
|
const ABSTAIN = { action: "abstain" };
|
|
43
43
|
/** How deep `sh -c`/`eval` lines are followed, and how many are read. */
|
|
44
44
|
const MAX_INNER_DEPTH = 16;
|
|
@@ -104,7 +104,8 @@ function globRegex(glob) {
|
|
|
104
104
|
// case-folded: on a disk that folds case, `.KISO/*` is the same directory
|
|
105
105
|
return new RegExp(`^${re}$`, "i");
|
|
106
106
|
}
|
|
107
|
-
function probeFor(id) {
|
|
107
|
+
function probeFor(id, { msys, path }) {
|
|
108
|
+
const { basename, dirname, join } = path;
|
|
108
109
|
const files = new Set();
|
|
109
110
|
const dirs = new Set();
|
|
110
111
|
const targets = [];
|
|
@@ -115,7 +116,7 @@ function probeFor(id) {
|
|
|
115
116
|
try {
|
|
116
117
|
for (const name of readdirSync(dir))
|
|
117
118
|
if (name.startsWith(`${base}.`))
|
|
118
|
-
targets.push(`${dir}/${name}`);
|
|
119
|
+
targets.push(msys ? join(dir, name) : `${dir}/${name}`);
|
|
119
120
|
}
|
|
120
121
|
catch {
|
|
121
122
|
// no directory yet — nothing in it to serve
|
|
@@ -139,8 +140,10 @@ function probeFor(id) {
|
|
|
139
140
|
export function protectedShellCheck(line, workspaceRoot, id, env) {
|
|
140
141
|
if (id.paths.length === 0)
|
|
141
142
|
return { hit: false };
|
|
142
|
-
const
|
|
143
|
-
const
|
|
143
|
+
const dialect = hostDialect();
|
|
144
|
+
const P = dialect.path;
|
|
145
|
+
const probe = probeFor(id, dialect);
|
|
146
|
+
const names = id.paths.map((p) => P.basename(p).toLowerCase());
|
|
144
147
|
const paths = id.paths.map((p) => p.toLowerCase());
|
|
145
148
|
const namesProtected = (t) => {
|
|
146
149
|
const low = t.toLowerCase();
|
|
@@ -168,7 +171,17 @@ export function protectedShellCheck(line, workspaceRoot, id, env) {
|
|
|
168
171
|
seenStat.set(p, st);
|
|
169
172
|
return st;
|
|
170
173
|
};
|
|
171
|
-
|
|
174
|
+
// on Windows the system normalizes a path lexically before it opens it,
|
|
175
|
+
// so the lexical resolution is what a command opens there
|
|
176
|
+
const at = (cwd, t) => (dialect.msys ? P.resolve(cwd, t) : t.startsWith("/") ? t : `${cwd}/${t}`);
|
|
177
|
+
/** Windows P2: a word as Git Bash's command sees the path — or null
|
|
178
|
+
* when it cannot be read (only its spelling can be checked). */
|
|
179
|
+
const native = (t) => {
|
|
180
|
+
if (!dialect.msys)
|
|
181
|
+
return t;
|
|
182
|
+
const read = msysPath(t);
|
|
183
|
+
return "opaque" in read ? null : read.native;
|
|
184
|
+
};
|
|
172
185
|
const regexes = new Map();
|
|
173
186
|
const realOf = new Map();
|
|
174
187
|
/** Whether a word's last component is a protected file's name, or one
|
|
@@ -183,12 +196,15 @@ export function protectedShellCheck(line, workspaceRoot, id, env) {
|
|
|
183
196
|
return out.replace(/\$\{KISO_HOME\}|\$KISO_HOME(?![A-Za-z0-9_])/g, env.kisoHome);
|
|
184
197
|
};
|
|
185
198
|
/** One literal-or-glob path text, read from each candidate directory. */
|
|
186
|
-
const pathHits = (
|
|
187
|
-
if (
|
|
199
|
+
const pathHits = (word, s) => {
|
|
200
|
+
if (word === "")
|
|
188
201
|
return false;
|
|
189
|
-
if (/[$`]/.test(
|
|
190
|
-
return namesProtected(
|
|
191
|
-
|
|
202
|
+
if (/[$`]/.test(word))
|
|
203
|
+
return namesProtected(word);
|
|
204
|
+
const t = native(word);
|
|
205
|
+
if (t === null)
|
|
206
|
+
return namesProtected(word);
|
|
207
|
+
if (s.lossy && !P.isAbsolute(t) && namesProtected(t))
|
|
192
208
|
return true;
|
|
193
209
|
const glob = /[*?[]/.test(t);
|
|
194
210
|
for (const cwd of s.cwds) {
|
|
@@ -198,13 +214,31 @@ export function protectedShellCheck(line, workspaceRoot, id, env) {
|
|
|
198
214
|
// not exist yet) — only when the last component could be the
|
|
199
215
|
// file's name — then by inode
|
|
200
216
|
if (named(t)) {
|
|
201
|
-
const spelled = resolve(cwd, t).toLowerCase();
|
|
217
|
+
const spelled = P.resolve(cwd, t).toLowerCase();
|
|
202
218
|
if (paths.some((pf) => spelled === pf || spelled.startsWith(`${pf}.`)))
|
|
203
219
|
return true;
|
|
204
220
|
}
|
|
205
221
|
const st = statAt(at(cwd, t));
|
|
206
222
|
if (st?.isFile() === true && probe.files.has(inode(st)))
|
|
207
223
|
return true;
|
|
224
|
+
if (dialect.msys && t.split("/").includes("..")) {
|
|
225
|
+
// Windows: the lexical reading above, and — for a `..`, where
|
|
226
|
+
// they can differ — the real-path one; a `..` after a link the
|
|
227
|
+
// two read apart falls back to names. One more reading charged.
|
|
228
|
+
charge();
|
|
229
|
+
const real = resolveShellPath(cwd, cwd, t, dialect);
|
|
230
|
+
if (real.opaque !== undefined) {
|
|
231
|
+
if (namesProtected(t))
|
|
232
|
+
return true;
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
const low = real.canonical.toLowerCase();
|
|
236
|
+
if (paths.some((pf) => low === pf || low.startsWith(`${pf}.`)))
|
|
237
|
+
return true;
|
|
238
|
+
const rst = statAt(real.canonical);
|
|
239
|
+
if (rst?.isFile() === true && probe.files.has(inode(rst)))
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
208
242
|
continue;
|
|
209
243
|
}
|
|
210
244
|
const g = t.search(/[*?[]/);
|
|
@@ -232,11 +266,14 @@ export function protectedShellCheck(line, workspaceRoot, id, env) {
|
|
|
232
266
|
}
|
|
233
267
|
realOf.set(dirText, real);
|
|
234
268
|
}
|
|
235
|
-
|
|
269
|
+
// Windows: one separator, one case, on both sides of the match
|
|
270
|
+
const fold = (p) => (dialect.msys ? p.replace(/\\/g, "/").toLowerCase() : p);
|
|
271
|
+
const dirReal = fold(real);
|
|
272
|
+
const pattern = `${dirReal.endsWith("/") ? dirReal : `${dirReal}/`}${dialect.msys ? rest.toLowerCase() : rest}`;
|
|
236
273
|
let re = regexes.get(pattern);
|
|
237
274
|
if (re === undefined)
|
|
238
275
|
regexes.set(pattern, (re = globRegex(pattern)));
|
|
239
|
-
if (probe.targets.some((p) => re.test(p)))
|
|
276
|
+
if (probe.targets.some((p) => re.test(fold(p))))
|
|
240
277
|
return true;
|
|
241
278
|
}
|
|
242
279
|
return false;
|
|
@@ -255,8 +292,11 @@ export function protectedShellCheck(line, workspaceRoot, id, env) {
|
|
|
255
292
|
/** A directory holding a protected file — the file's own, or any
|
|
256
293
|
* directory above it — by inode. */
|
|
257
294
|
const holds = (w, s) => {
|
|
258
|
-
const
|
|
259
|
-
if (
|
|
295
|
+
const spelled = expand(w.text, w.tilde);
|
|
296
|
+
if (spelled === "" || /[$`*?[]/.test(spelled) || (w.tilde && /^~[^/]/.test(w.text)))
|
|
297
|
+
return false;
|
|
298
|
+
const t = native(spelled);
|
|
299
|
+
if (t === null)
|
|
260
300
|
return false;
|
|
261
301
|
return s.cwds.some((cwd) => {
|
|
262
302
|
charge();
|
|
@@ -334,8 +374,9 @@ export function protectedShellCheck(line, workspaceRoot, id, env) {
|
|
|
334
374
|
else if (to === undefined)
|
|
335
375
|
s.pending = [env.home];
|
|
336
376
|
else {
|
|
337
|
-
const
|
|
338
|
-
|
|
377
|
+
const spelled = expand(to.text, to.tilde);
|
|
378
|
+
const t = native(spelled);
|
|
379
|
+
if (t === null || /[$`*?[]/.test(spelled) || (to.tilde && /^~[^/]/.test(to.text))) {
|
|
339
380
|
s.lossy = true;
|
|
340
381
|
s.pending = cap(s, [env.home, ...s.cwds]);
|
|
341
382
|
}
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* So a write_file or edit_file whose path — as written, or once symlinks
|
|
13
13
|
* are followed — passes through a `.git` or `.kiso` directory asks in
|
|
14
14
|
* every asking tier: accept-edits' allow does not cover it, and neither
|
|
15
|
-
* does a saved allow.
|
|
16
|
-
*
|
|
15
|
+
* does a saved allow. With the don't-ask switch on it is therefore denied;
|
|
16
|
+
* plan denies it anyway; full-access runs it, as full-access's shell could.
|
|
17
17
|
*/
|
|
18
18
|
import type { PolicyCall } from "@vincemakes/kiso-core";
|
|
19
19
|
import type { KisoExtension } from "@vincemakes/kiso-runtime";
|
package/dist/protected-writes.js
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* So a write_file or edit_file whose path — as written, or once symlinks
|
|
13
13
|
* are followed — passes through a `.git` or `.kiso` directory asks in
|
|
14
14
|
* every asking tier: accept-edits' allow does not cover it, and neither
|
|
15
|
-
* does a saved allow.
|
|
16
|
-
*
|
|
15
|
+
* does a saved allow. With the don't-ask switch on it is therefore denied;
|
|
16
|
+
* plan denies it anyway; full-access runs it, as full-access's shell could.
|
|
17
17
|
*/
|
|
18
18
|
import { relative, sep } from "node:path";
|
|
19
19
|
import { resolveShellPath } from "./shell-words.js";
|
package/dist/readonly-shell.js
CHANGED
|
@@ -32,24 +32,26 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import { statSync } from "node:fs";
|
|
34
34
|
import { homedir } from "node:os";
|
|
35
|
-
import {
|
|
35
|
+
import { join } from "node:path";
|
|
36
36
|
import { isCredentialName, isCredentialPath } from "@vincemakes/kiso-tools-node";
|
|
37
37
|
import { getMode } from "./mode.js";
|
|
38
|
-
import {
|
|
38
|
+
import { homeSubtrees, hostDialect, parseShell, realCase, resolveShellPath } from "./shell-words.js";
|
|
39
39
|
// ── the predicates ──────────────────────────────────────────────────────
|
|
40
40
|
function scope(ctx, word) {
|
|
41
41
|
if (word === "-")
|
|
42
42
|
return null; // stdin
|
|
43
43
|
for (const cwd of ctx.cwds) {
|
|
44
|
-
const { canonical, inside } = resolveShellPath(ctx.root, cwd, word);
|
|
44
|
+
const { canonical, inside, opaque } = resolveShellPath(ctx.root, cwd, word);
|
|
45
|
+
if (opaque !== undefined)
|
|
46
|
+
return `${word} is ${opaque}`;
|
|
45
47
|
if (!inside)
|
|
46
48
|
return `${word} is outside the workspace`;
|
|
47
49
|
// B4: under a protected directory NOTHING is read or listed unasked —
|
|
48
50
|
// kiso's own home, and ~/.ssh ~/.config ~/.aws ~/.gnupg ~/.kiso when
|
|
49
51
|
// the workspace is the home directory. Over-asking is an ask.
|
|
50
52
|
for (const p of ctx.protectedRoots) {
|
|
51
|
-
const rel = relative(p, canonical);
|
|
52
|
-
if (rel === "" || (!rel.startsWith("..") && !isAbsolute(rel)))
|
|
53
|
+
const rel = hostDialect().path.relative(p, canonical);
|
|
54
|
+
if (rel === "" || (!rel.startsWith("..") && !hostDialect().path.isAbsolute(rel)))
|
|
53
55
|
return `${word} is inside a protected directory (${p})`;
|
|
54
56
|
}
|
|
55
57
|
}
|
|
@@ -59,7 +61,7 @@ function scope(ctx, word) {
|
|
|
59
61
|
* the file tools ask ONE rule — names (`isCredentialName`) and the
|
|
60
62
|
* directory-scoped paths (`isCredentialPath`) — on the canonical path. */
|
|
61
63
|
function shellCredential(canonical) {
|
|
62
|
-
return isCredentialName(basename(canonical).toLowerCase()) || isCredentialPath(canonical);
|
|
64
|
+
return isCredentialName(hostDialect().path.basename(canonical).toLowerCase()) || isCredentialPath(canonical);
|
|
63
65
|
}
|
|
64
66
|
function content(ctx, word) {
|
|
65
67
|
const s = scope(ctx, word);
|
|
@@ -69,11 +71,11 @@ function content(ctx, word) {
|
|
|
69
71
|
return null;
|
|
70
72
|
// B2: the disk is case-insensitive and the names are not — `.ENV` and
|
|
71
73
|
// `ID_RSA` are `.env` and `id_rsa` to the file system, so to the rule
|
|
72
|
-
if (isCredentialName(basename(word).toLowerCase()))
|
|
74
|
+
if (isCredentialName(hostDialect().path.basename(word).toLowerCase()))
|
|
73
75
|
return `${word} is a credential file`;
|
|
74
76
|
for (const cwd of ctx.cwds) {
|
|
75
77
|
const { canonical } = resolveShellPath(ctx.root, cwd, word);
|
|
76
|
-
if (isCredentialName(basename(canonical).toLowerCase()) || shellCredential(canonical))
|
|
78
|
+
if (isCredentialName(hostDialect().path.basename(canonical).toLowerCase()) || shellCredential(canonical))
|
|
77
79
|
return `${word} leads to a credential file`;
|
|
78
80
|
}
|
|
79
81
|
return null;
|
|
@@ -552,11 +554,11 @@ export function classifyReadOnly(commandLine, workspaceRoot, protectedRoots = []
|
|
|
552
554
|
}
|
|
553
555
|
return { allow: true };
|
|
554
556
|
}
|
|
555
|
-
/** The modes in which a provable read runs unasked
|
|
556
|
-
* allow is what it still honours. Manual promises that
|
|
557
|
-
* plan denies shell outright;
|
|
558
|
-
* member has nothing to add to any of them. */
|
|
559
|
-
const ALLOWING_MODES = new Set(["default", "accept-edits"
|
|
557
|
+
/** The modes in which a provable read runs unasked — with the don't-ask
|
|
558
|
+
* switch on too: an allow is what it still honours. Manual promises that
|
|
559
|
+
* every tool asks; plan denies shell outright; full-access allows
|
|
560
|
+
* everything already — this member has nothing to add to any of them. */
|
|
561
|
+
const ALLOWING_MODES = new Set(["default", "accept-edits"]);
|
|
560
562
|
const ABSTAIN = { action: "abstain" };
|
|
561
563
|
/**
|
|
562
564
|
* The chain member. ALLOW or ABSTAIN, never anything else: an allow here
|
|
@@ -577,7 +579,7 @@ export function readOnlyShellExtension(roots) {
|
|
|
577
579
|
if (typeof command !== "string")
|
|
578
580
|
return ABSTAIN;
|
|
579
581
|
const { workspaceRoot, excludeRoots } = roots();
|
|
580
|
-
const protectedRoots = [...excludeRoots, ...
|
|
582
|
+
const protectedRoots = [...excludeRoots, ...homeSubtrees().map((s) => join(homedir(), s))];
|
|
581
583
|
return classifyReadOnly(command, workspaceRoot, protectedRoots).allow ? { action: "allow" } : ABSTAIN;
|
|
582
584
|
},
|
|
583
585
|
},
|
package/dist/replay.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* the viewer — then any uncovered earlier turns folded as above.
|
|
21
21
|
*/
|
|
22
22
|
import { echoText } from "@vincemakes/kiso-tui-cells/render";
|
|
23
|
+
import { taskNoticeRow } from "./task-notice.js";
|
|
23
24
|
/** How many turns stay on screen in full. */
|
|
24
25
|
const FULL_TURNS = 2;
|
|
25
26
|
/** A user turn's opening line, or null when this input does not open one
|
|
@@ -61,6 +62,8 @@ function replayTurn(body, turn) {
|
|
|
61
62
|
const ask = askOf(e);
|
|
62
63
|
if (ask !== null)
|
|
63
64
|
body.userLine(ask);
|
|
65
|
+
else if (e.via?.kind === "tasks")
|
|
66
|
+
body.notice(taskNoticeRow(e.via.items));
|
|
64
67
|
else if (e.source === "system") {
|
|
65
68
|
body.notice("verification pass");
|
|
66
69
|
body.notice(` ${typeof e.content === "string" ? e.content : ""}`);
|
package/dist/resume.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* moved verbatim from index.ts.
|
|
4
4
|
*/
|
|
5
5
|
import { idleStatus, runningStatus } from "@vincemakes/kiso-tui";
|
|
6
|
-
import {
|
|
6
|
+
import { modeDisplay } from "./mode.js";
|
|
7
7
|
import { agentModel, dock, retryOnRow, floorOn } from "./state.js";
|
|
8
8
|
import { pendingAsk, resolveUncertains } from "./trust-ui.js";
|
|
9
9
|
import { failOnFauxExhaustion } from "./faux-glue.js";
|
|
@@ -37,7 +37,7 @@ export async function resume(session, prompt, faux, input) {
|
|
|
37
37
|
// same budget. It has no meter, so nothing here can be dropped that
|
|
38
38
|
// was not already at risk of being cut.
|
|
39
39
|
if (dock.active)
|
|
40
|
-
dock.setStatus(idleStatus(
|
|
40
|
+
dock.setStatus(idleStatus(modeDisplay(), agentModel, estimateCtxRatio(session), undefined, process.stdout.columns > 0 ? process.stdout.columns : 80, !floorOn));
|
|
41
41
|
};
|
|
42
42
|
const withRun = async (run) => {
|
|
43
43
|
currentRun = run;
|
package/dist/settings.d.ts
CHANGED
|
@@ -20,8 +20,11 @@ export interface SettingsInput {
|
|
|
20
20
|
/** the --mode / --model values on the command line, when given */
|
|
21
21
|
readonly modeFlag?: string;
|
|
22
22
|
readonly modelFlag?: string;
|
|
23
|
+
/** whether --dont-ask was on the command line */
|
|
24
|
+
readonly dontAskFlag?: boolean;
|
|
23
25
|
/** what is live now */
|
|
24
26
|
readonly mode: string;
|
|
27
|
+
readonly dontAsk?: boolean;
|
|
25
28
|
readonly model: {
|
|
26
29
|
readonly label: string;
|
|
27
30
|
readonly profile: string | null;
|
package/dist/settings.js
CHANGED
|
@@ -1,18 +1,47 @@
|
|
|
1
|
+
import { envModeLayer, parseMode, resolveModeLayers } from "./mode.js";
|
|
2
|
+
const MODE_LAYER = { flag: "--mode", env: "env KISO_MODE", project: "project config", user: "user config", default: "default" };
|
|
3
|
+
/** What a layer wrote for the tier — an old name is named, so a person
|
|
4
|
+
* learns the new one from the row that shows it. */
|
|
5
|
+
function writtenMode(i, source) {
|
|
6
|
+
return source === "flag" ? i.modeFlag : source === "env" ? i.env.KISO_MODE : source === "project" ? i.project?.mode : source === "user" ? i.user?.mode : undefined;
|
|
7
|
+
}
|
|
8
|
+
function modeFrom(i, source) {
|
|
9
|
+
const w = writtenMode(i, source);
|
|
10
|
+
return w === "bypass" || w === "dontAsk" ? `${MODE_LAYER[source]} (written "${w}")` : MODE_LAYER[source];
|
|
11
|
+
}
|
|
12
|
+
/** The switch's layer: its own key there, or — the resolution's other
|
|
13
|
+
* way in — that layer's tier written as the old name dontAsk. */
|
|
14
|
+
function dontAskFrom(i, source) {
|
|
15
|
+
if (source === "default")
|
|
16
|
+
return "default";
|
|
17
|
+
const own = source === "flag" ? i.dontAskFlag === true : source === "env" ? envModeLayer(i.env).dontAsk !== undefined : source === "project" ? i.project?.dontAsk !== undefined : i.user?.dontAsk !== undefined;
|
|
18
|
+
if (own)
|
|
19
|
+
return source === "flag" ? "--dont-ask" : source === "env" ? "env KISO_DONT_ASK" : MODE_LAYER[source];
|
|
20
|
+
return `${MODE_LAYER[source]} (written "dontAsk")`;
|
|
21
|
+
}
|
|
22
|
+
function modeLayers(i) {
|
|
23
|
+
return {
|
|
24
|
+
flag: { ...(i.modeFlag !== undefined ? { mode: i.modeFlag } : {}), ...(i.dontAskFlag === true ? { dontAsk: true } : {}) },
|
|
25
|
+
env: envModeLayer(i.env),
|
|
26
|
+
project: i.project,
|
|
27
|
+
user: i.user,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
1
30
|
function modeRow(i) {
|
|
2
31
|
const change = "/mode or shift+tab; \"mode\" in ~/.kiso/config.json";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
: i.project?.mode === i.mode
|
|
8
|
-
? "project config"
|
|
9
|
-
: i.user?.mode === i.mode
|
|
10
|
-
? "user config"
|
|
11
|
-
: i.mode === "default"
|
|
12
|
-
? "default"
|
|
13
|
-
: "set in this session";
|
|
32
|
+
// the layers decided at start-up; a live tier they do not explain was
|
|
33
|
+
// set in this session
|
|
34
|
+
const resolved = resolveModeLayers(modeLayers(i));
|
|
35
|
+
const from = parseMode(i.mode)?.mode === resolved.mode ? modeFrom(i, resolved.from.mode) : "set in this session";
|
|
14
36
|
return { name: "mode", value: i.mode, from, change };
|
|
15
37
|
}
|
|
38
|
+
function dontAskRow(i) {
|
|
39
|
+
const change = "/dont-ask; --dont-ask; \"dontAsk\": true in ~/.kiso/config.json";
|
|
40
|
+
const on = i.dontAsk === true;
|
|
41
|
+
const resolved = resolveModeLayers(modeLayers(i));
|
|
42
|
+
const from = (resolved.dontAsk !== "off") === on ? dontAskFrom(i, resolved.from.dontAsk) : "set in this session";
|
|
43
|
+
return { name: "don't ask", value: on ? "on — what would ask is refused" : "off", from, change };
|
|
44
|
+
}
|
|
16
45
|
function modelRow(i) {
|
|
17
46
|
const change = "/model; \"model\" in ~/.kiso/config.json";
|
|
18
47
|
const p = i.model.profile;
|
|
@@ -50,6 +79,7 @@ export function settingsRows(i) {
|
|
|
50
79
|
const rows = [
|
|
51
80
|
modelRow(i),
|
|
52
81
|
modeRow(i),
|
|
82
|
+
dontAskRow(i),
|
|
53
83
|
themeRow(i),
|
|
54
84
|
{
|
|
55
85
|
name: "floor",
|