@vincemakes/kiso-code 0.45.3 → 0.46.1
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 +13 -1
- package/dist/dispatch.d.ts +2 -0
- package/dist/dispatch.js +170 -33
- 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/preferences.d.ts +14 -0
- package/dist/preferences.js +34 -3
- 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.d.ts
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,19 +7,37 @@
|
|
|
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 type { KisoExtension } from "@vincemakes/kiso-runtime";
|
|
13
|
-
export type Mode = "manual" | "default" | "accept-edits" | "plan" | "
|
|
14
|
-
/** Every tier kiso
|
|
15
|
-
* `/mode <name>`. */
|
|
21
|
+
export type Mode = "manual" | "default" | "accept-edits" | "plan" | "full-access";
|
|
22
|
+
/** Every tier kiso decides with. */
|
|
16
23
|
export declare const MODES: readonly Mode[];
|
|
17
|
-
/** The tiers kiso OFFERS a person
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* breaks. `dontAsk` joined: the unattended tier. */
|
|
24
|
+
/** The tiers kiso OFFERS a person: the picker, shift+tab, the printed
|
|
25
|
+
* list, the help. `manual` left the offer at 0.40.0 — a saved allow
|
|
26
|
+
* outranks its ask, so "asks for every tool" was never quite what it
|
|
27
|
+
* did — and stays accepted, so no config that names it breaks. */
|
|
22
28
|
export declare const OFFERED_MODES: readonly Mode[];
|
|
29
|
+
/** What a person reads for each tier — the status row, the picker, the
|
|
30
|
+
* notices. The typed value (config, --mode, KISO_MODE) keeps its hyphen. */
|
|
31
|
+
export declare const MODE_LABEL: Readonly<Record<Mode, string>>;
|
|
32
|
+
/** One tier plus, for the one spelling that carried it, the switch. */
|
|
33
|
+
export interface ModeSetting {
|
|
34
|
+
readonly mode: Mode;
|
|
35
|
+
readonly dontAsk?: true;
|
|
36
|
+
}
|
|
37
|
+
/** Every value a config, KISO_MODE, --mode or `/mode <name>` accepts. */
|
|
38
|
+
export declare const MODE_VALUES: readonly string[];
|
|
39
|
+
/** A typed tier: its value, its label ("full access"), or an old name. */
|
|
40
|
+
export declare function parseMode(raw: string): ModeSetting | undefined;
|
|
23
41
|
/** DC-36 — one line per tier, for the picker, TRANSCRIBED FROM decide()
|
|
24
42
|
* below rather than written fresh. A description that drifts from the
|
|
25
43
|
* behaviour is worse than none: this is the row a human reads before
|
|
@@ -34,21 +52,76 @@ export declare const OFFERED_MODES: readonly Mode[];
|
|
|
34
52
|
* is unchanged; the copy was the thing that was wrong. `plan` needs no
|
|
35
53
|
* qualification because it DENIES, and a deny is what nothing overrides. */
|
|
36
54
|
export declare const MODE_NOTE: Readonly<Record<Mode, string>>;
|
|
55
|
+
/** The switch in one line, for a surface that describes it, under the
|
|
56
|
+
* same 60-column rule. The /mode panel does not offer it: don't ask is
|
|
57
|
+
* not a tier, so it never sits beside them (owner, 2026-09-30) — the
|
|
58
|
+
* panel's header names it when it is on. */
|
|
59
|
+
export declare const DONT_ASK_NOTE = "never asks: what would ask is refused and the run goes on";
|
|
60
|
+
/** The switch. "old-name" is a switch that arrived bundled with the old
|
|
61
|
+
* `dontAsk` tier: it leaves with the tier, as leaving dontAsk always
|
|
62
|
+
* gave the asks back. A switch set on its own ("on") outlives every tier
|
|
63
|
+
* change. */
|
|
64
|
+
export type DontAskState = "off" | "on" | "old-name";
|
|
37
65
|
export declare function getMode(): Mode;
|
|
66
|
+
/** A tier switch. The switch stays as it is — unless it came with the
|
|
67
|
+
* old dontAsk tier, which this switch leaves. */
|
|
38
68
|
export declare function setMode(m: Mode): void;
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
69
|
+
/** A typed tier, old names included: `/mode dontAsk` is default with the
|
|
70
|
+
* switch on, exactly the tier it used to be. */
|
|
71
|
+
export declare function applyModeSetting(s: ModeSetting): void;
|
|
72
|
+
/** Whether kiso may stop for a person — every ask endpoint reads this. */
|
|
73
|
+
export declare function getDontAsk(): boolean;
|
|
74
|
+
export declare function setDontAsk(on: boolean): void;
|
|
75
|
+
/** The tier as the status row and the notices name it, with the switch
|
|
76
|
+
* beside it when it is on. */
|
|
77
|
+
export declare function modeDisplay(): string;
|
|
78
|
+
/** One layer's say: what a config file, the environment or the command
|
|
79
|
+
* line wrote. KisoConfig is one of these. */
|
|
80
|
+
export interface ModeLayer {
|
|
81
|
+
readonly mode?: string | undefined;
|
|
82
|
+
readonly dontAsk?: boolean | undefined;
|
|
83
|
+
}
|
|
84
|
+
export type ModeSource = "flag" | "env" | "project" | "user" | "default";
|
|
85
|
+
export interface ModeState {
|
|
86
|
+
readonly mode: Mode;
|
|
87
|
+
readonly dontAsk: DontAskState;
|
|
88
|
+
/** the layer each answer came from — /settings names it */
|
|
89
|
+
readonly from: {
|
|
90
|
+
readonly mode: ModeSource;
|
|
91
|
+
readonly dontAsk: ModeSource;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** KISO_MODE and KISO_DONT_ASK. A value kiso cannot read is no value —
|
|
95
|
+
* the next layer decides, as an unknown KISO_MODE always did. */
|
|
96
|
+
export declare function envModeLayer(env?: Readonly<Record<string, string | undefined>>): ModeLayer;
|
|
97
|
+
/**
|
|
98
|
+
* Precedence: flag > env > project config > user config > default, for
|
|
99
|
+
* each answer on its own. The tier comes from the highest layer that
|
|
100
|
+
* names a readable one. The switch comes from the highest layer that
|
|
101
|
+
* either sets it (`--dont-ask`, KISO_DONT_ASK, "dontAsk": true|false) or
|
|
102
|
+
* WON THE TIER with the old name `dontAsk` — an old name's switch counts
|
|
103
|
+
* only where that old name decided the tier. So every combination of old
|
|
104
|
+
* spellings resolves as it did when dontAsk was a tier: a user config
|
|
105
|
+
* saying "dontAsk" under `--mode bypass` still asks, because --mode won.
|
|
106
|
+
*/
|
|
107
|
+
export declare function resolveModeLayers(layers: {
|
|
108
|
+
readonly flag?: ModeLayer | null | undefined;
|
|
109
|
+
readonly env?: ModeLayer | null | undefined;
|
|
110
|
+
readonly project?: ModeLayer | null | undefined;
|
|
111
|
+
readonly user?: ModeLayer | null | undefined;
|
|
112
|
+
}): ModeState;
|
|
113
|
+
/** Put a resolved state in force — startup, and again once the project
|
|
114
|
+
* config is known. */
|
|
115
|
+
export declare function applyModeState(s: ModeState): void;
|
|
116
|
+
/** The built-in mode tiers as chain extensions — named "mode:<tier>"
|
|
44
117
|
* so the runtime's decidedBy records exactly that (the runtime derives
|
|
45
118
|
* approvalPolicies from extensions[].approvals, tagging each with the
|
|
46
119
|
* extension name). The CURRENT tier is first: an all-allow chain records
|
|
47
120
|
* decidedBy = the FIRST SPEAKER, so an auto-allow under the startup mode
|
|
48
121
|
* names that mode honestly. Order never affects verdicts — the chain is
|
|
49
122
|
* deny>allow>ask over the SPEAKING verdicts (abstain = no opinion), so a
|
|
50
|
-
* user extension's deny wins over any mode tier,
|
|
51
|
-
* monotonicity e2e pins it). */
|
|
123
|
+
* user extension's deny wins over any mode tier, full-access included
|
|
124
|
+
* (the monotonicity e2e pins it). */
|
|
52
125
|
export declare function modeExtensions(workspaceRoot?: () => string): readonly KisoExtension[];
|
|
53
126
|
/** The plan tier's system prompt addition — injected at startup when the
|
|
54
127
|
* initial mode is plan (the session prompt is fixed at creation; runtime
|
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/preferences.d.ts
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import type { ModelProfile } from "./config.js";
|
|
1
2
|
export interface Preferences {
|
|
2
3
|
/** ctrl+t: the thinking blocks as one italic line each. Absent = shown. */
|
|
3
4
|
readonly thinking?: "shown" | "hidden";
|
|
5
|
+
/** B1: the effort last picked for each profile (`/model <p> <e>`, or the
|
|
6
|
+
* panel with a level) — where a NEW session of that profile starts. */
|
|
7
|
+
readonly effort?: Readonly<Record<string, string>>;
|
|
4
8
|
}
|
|
5
9
|
export declare function preferencesPath(home?: string): string;
|
|
6
10
|
/** Startup: read the file, and keep choices made from here on in it. */
|
|
@@ -10,5 +14,15 @@ export declare function preferences(): Preferences;
|
|
|
10
14
|
/** Remember a choice. Returns false when no file was named. The file is
|
|
11
15
|
* re-read before the write, so another process's choice is kept. */
|
|
12
16
|
export declare function setPreference<K extends keyof Preferences>(key: K, value: NonNullable<Preferences[K]>): boolean;
|
|
17
|
+
/** B1: remember an explicit effort pick for a profile. */
|
|
18
|
+
export declare function rememberEffort(profile: string, effort: string): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* B1: the effort a NEW session of `profile` starts at, from the one last
|
|
21
|
+
* picked — the picker's fallback: the level itself when the model has it,
|
|
22
|
+
* else the endpoint's registry default, else none (default/default, no
|
|
23
|
+
* reasoning key). Only a level that resolves for the model is returned, so
|
|
24
|
+
* the session never carries a setting its first request would refuse.
|
|
25
|
+
*/
|
|
26
|
+
export declare function startingEffort(profile: Pick<ModelProfile, "model" | "baseUrl">, remembered: string | undefined): string | null;
|
|
13
27
|
/** Tests: back to the state before startup. */
|
|
14
28
|
export declare function resetPreferences(): void;
|
package/dist/preferences.js
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* `<KISO_HOME>/preferences.json`.
|
|
4
4
|
*
|
|
5
5
|
* `config.json` is the human's file: kiso reads it and never writes it. A
|
|
6
|
-
* choice made with a key — ctrl+t's thinking display,
|
|
7
|
-
* has to survive a restart, so it lands here instead: kiso-owned, private
|
|
6
|
+
* choice made with a key — ctrl+t's thinking display, and (B1) the effort
|
|
7
|
+
* last picked for each profile — has to survive a restart, so it lands here instead: kiso-owned, private
|
|
8
8
|
* (0600, tmp + rename), and read back at start-up. A missing or unreadable
|
|
9
9
|
* file is an empty one; an unknown value is ignored rather than guessed.
|
|
10
10
|
*
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { dirname, join } from "node:path";
|
|
17
|
+
import { lookupModelMetadata, resolveReasoning } from "@vincemakes/kiso-runtime/internal";
|
|
17
18
|
import { kisoHome } from "./state.js";
|
|
18
19
|
let prefs = null;
|
|
19
20
|
let prefsPath = null;
|
|
@@ -23,7 +24,15 @@ export function preferencesPath(home = kisoHome()) {
|
|
|
23
24
|
function read(path) {
|
|
24
25
|
try {
|
|
25
26
|
const raw = JSON.parse(readFileSync(path, "utf8"));
|
|
26
|
-
|
|
27
|
+
const out = {};
|
|
28
|
+
if (raw.thinking === "shown" || raw.thinking === "hidden")
|
|
29
|
+
out.thinking = raw.thinking;
|
|
30
|
+
if (raw.effort !== null && typeof raw.effort === "object" && !Array.isArray(raw.effort)) {
|
|
31
|
+
const effort = Object.fromEntries(Object.entries(raw.effort).filter((e) => typeof e[1] === "string"));
|
|
32
|
+
if (Object.keys(effort).length > 0)
|
|
33
|
+
out.effort = effort;
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
27
36
|
}
|
|
28
37
|
catch {
|
|
29
38
|
return {};
|
|
@@ -57,6 +66,28 @@ export function setPreference(key, value) {
|
|
|
57
66
|
}
|
|
58
67
|
return true;
|
|
59
68
|
}
|
|
69
|
+
/** B1: remember an explicit effort pick for a profile. */
|
|
70
|
+
export function rememberEffort(profile, effort) {
|
|
71
|
+
return setPreference("effort", { ...(preferences().effort ?? {}), [profile]: effort });
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* B1: the effort a NEW session of `profile` starts at, from the one last
|
|
75
|
+
* picked — the picker's fallback: the level itself when the model has it,
|
|
76
|
+
* else the endpoint's registry default, else none (default/default, no
|
|
77
|
+
* reasoning key). Only a level that resolves for the model is returned, so
|
|
78
|
+
* the session never carries a setting its first request would refuse.
|
|
79
|
+
*/
|
|
80
|
+
export function startingEffort(profile, remembered) {
|
|
81
|
+
if (remembered === undefined || remembered === "default")
|
|
82
|
+
return null;
|
|
83
|
+
const effort = lookupModelMetadata(profile.model, profile.baseUrl)?.capabilities.reasoning?.effort ?? null;
|
|
84
|
+
if (effort === null)
|
|
85
|
+
return null;
|
|
86
|
+
const legal = (level) => resolveReasoning(profile.model, { thinking: "default", effort: level }, profile.baseUrl).ok;
|
|
87
|
+
if (effort.levels.includes(remembered) && legal(remembered))
|
|
88
|
+
return remembered;
|
|
89
|
+
return effort.default !== null && legal(effort.default) ? effort.default : null;
|
|
90
|
+
}
|
|
60
91
|
/** Tests: back to the state before startup. */
|
|
61
92
|
export function resetPreferences() {
|
|
62
93
|
prefs = null;
|
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";
|