@vincemakes/kiso-code 0.39.1 → 0.40.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 +1 -1
- package/dist/attachments.d.ts +1 -1
- package/dist/attachments.js +11 -4
- package/dist/builtin.d.ts +12 -0
- package/dist/builtin.js +22 -1
- package/dist/chat.d.ts +28 -2
- package/dist/chat.js +143 -25
- package/dist/config.d.ts +11 -1
- package/dist/config.js +48 -3
- package/dist/dispatch.d.ts +10 -2
- package/dist/dispatch.js +138 -23
- package/dist/floor.d.ts +80 -0
- package/dist/floor.js +652 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.js +397 -44
- package/dist/mode.d.ts +12 -4
- package/dist/mode.js +32 -10
- package/dist/projects.d.ts +106 -0
- package/dist/projects.js +212 -0
- package/dist/protected-shell.d.ts +88 -0
- package/dist/protected-shell.js +406 -0
- package/dist/protected-writes.d.ts +29 -0
- package/dist/protected-writes.js +54 -0
- package/dist/readonly-shell.d.ts +58 -0
- package/dist/readonly-shell.js +590 -0
- package/dist/replay.d.ts +53 -0
- package/dist/replay.js +158 -0
- package/dist/resume-tail.js +4 -1
- package/dist/resume.js +3 -3
- package/dist/retries.d.ts +9 -0
- package/dist/retries.js +16 -0
- package/dist/session-cards.d.ts +33 -4
- package/dist/session-cards.js +44 -3
- package/dist/session-migration.d.ts +104 -0
- package/dist/session-migration.js +319 -0
- package/dist/shell-words.d.ts +134 -0
- package/dist/shell-words.js +628 -0
- package/dist/skill-invoke.d.ts +58 -0
- package/dist/skill-invoke.js +139 -0
- package/dist/state.d.ts +67 -1
- package/dist/state.js +98 -2
- package/dist/trust-ui.js +29 -5
- package/package.json +15 -15
package/dist/dispatch.d.ts
CHANGED
|
@@ -3,8 +3,10 @@
|
|
|
3
3
|
* turns. The bodies moved verbatim from chat()'s closure; chat provides
|
|
4
4
|
* the context (the chain, the run state, the prompt arming).
|
|
5
5
|
*/
|
|
6
|
+
import type { SessionRoute } from "./projects.js";
|
|
6
7
|
import type { AgentSession } from "@vincemakes/kiso-runtime";
|
|
7
8
|
import { type LineInput } from "./state.js";
|
|
9
|
+
import type { UserInputVia } from "@vincemakes/kiso-core";
|
|
8
10
|
/** Everything dispatch touches that chat() owns. */
|
|
9
11
|
export interface DispatchCtx {
|
|
10
12
|
readonly session: AgentSession;
|
|
@@ -23,8 +25,10 @@ export interface DispatchCtx {
|
|
|
23
25
|
* gone; a binding that has not run yet paints none — an unmeasured
|
|
24
26
|
* cache is not a 0% cache, and it is not the previous model's either. */
|
|
25
27
|
readonly modelSwitched: () => void;
|
|
26
|
-
/** submit a real turn: queue + chain (the turn closure lives in chat).
|
|
27
|
-
|
|
28
|
+
/** submit a real turn: queue + chain (the turn closure lives in chat).
|
|
29
|
+
* 0.40.0: `via` marks a skill turn — `line` is then the composed
|
|
30
|
+
* content, and the chip shows `via.line`. */
|
|
31
|
+
readonly submitTurn: (line: string, via?: UserInputVia) => void;
|
|
28
32
|
/** the /status context estimate. */
|
|
29
33
|
readonly estimateCtx: () => number;
|
|
30
34
|
/** TUI2-R1 (E): the model's context window, as the session is
|
|
@@ -38,6 +42,10 @@ export interface DispatchCtx {
|
|
|
38
42
|
readonly requestReload: () => void;
|
|
39
43
|
/** every durable session id (the /resume validation + listing). */
|
|
40
44
|
readonly sessions: () => readonly string[];
|
|
45
|
+
/** 0.40.0: an id outside this project's folder — refused with where to
|
|
46
|
+
* go, or opened where it is with a line (absent: every such id is "no
|
|
47
|
+
* such"). */
|
|
48
|
+
readonly route?: (id: string) => SessionRoute | null;
|
|
41
49
|
/** the dock's session picker, when one exists (bare /resume). */
|
|
42
50
|
readonly pickSession?: () => Promise<string | null>;
|
|
43
51
|
}
|
package/dist/dispatch.js
CHANGED
|
@@ -3,22 +3,53 @@
|
|
|
3
3
|
* turns. The bodies moved verbatim from chat()'s closure; chat provides
|
|
4
4
|
* the context (the chain, the run state, the prompt arming).
|
|
5
5
|
*/
|
|
6
|
-
import { contextRows, contextUnavailableRows, displayVerb, escapeTerminal, helpRows, kUnit, modePickView, modelPickView, palette, renderEvent, settledLabel, slashCommandNames } from "@vincemakes/kiso-tui";
|
|
6
|
+
import { STATUS_GLYPHS, contextRows, contextUnavailableRows, displayVerb, escapeTerminal, helpRows, kUnit, modePickView, modelPickView, compactingStatus, palette, renderEvent, settledLabel, slashCommandNames } from "@vincemakes/kiso-tui";
|
|
7
7
|
import { newSessionId } from "./session-id.js";
|
|
8
8
|
import { buildAdapter, lookupModelMetadata, resolveContinuationScope, resolveReasoning } from "@vincemakes/kiso-runtime/internal";
|
|
9
|
-
import { MODES, MODE_NOTE, getMode, setMode } from "./mode.js";
|
|
9
|
+
import { MODES, MODE_NOTE, OFFERED_MODES, getMode, setMode } from "./mode.js";
|
|
10
10
|
import { clipboardWrite, lastAnswer } from "./clipboard.js";
|
|
11
|
-
import {
|
|
11
|
+
import { protectedBangReason, protectedShellVerdict } from "./protected-shell.js";
|
|
12
|
+
import { agentModel, body, bodyLog, codingToolOptions, protectedFiles, kisoHome, configModels, dock, lastBinding, loadedSkillsCatalog, mergedConfig, readContextLedger, retryOnRow, sessionsDir, setAgentModel, setConfiguredWindow, setCurrentModelName, setModelChoice, setRetryShown, setLastBinding } from "./state.js";
|
|
12
13
|
import { adapterOptionsFor } from "./auth/adapter-options.js";
|
|
13
|
-
import { microcompactThresholdFor } from "./chat.js";
|
|
14
|
+
import { contextWindowTokens, microcompactThresholdFor, startStatusSpinner } from "./chat.js";
|
|
14
15
|
import { authForProfile, directWriteProfile, profileAvailable, resolveContextWindow, unavailableReason } from "./config.js";
|
|
15
16
|
import { shellTool } from "@vincemakes/kiso-tools-node";
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
+
import { dirname, join } from "node:path";
|
|
18
|
+
import { realpathSync } from "node:fs";
|
|
19
|
+
import { homedir } from "node:os";
|
|
20
|
+
import { resolveSkillLine, skillsRows } from "./skill-invoke.js";
|
|
21
|
+
/** Where a skill directory really lives. Project and user skills are
|
|
22
|
+
* merged into one scan directory by symlink, so the scan root names a
|
|
23
|
+
* temp directory; the link's target is the answer a person can use. */
|
|
24
|
+
function skillSource(dir) {
|
|
25
|
+
const scan = process.env.KISO_SKILLS_DIR ?? join(kisoHome(), "skills");
|
|
26
|
+
try {
|
|
27
|
+
return tildePath(dirname(realpathSync(join(scan, dir))));
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return tildePath(scan); // a broken link has no target to name
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function tildePath(p) {
|
|
34
|
+
const home = homedir();
|
|
35
|
+
return p === home || p.startsWith(`${home}/`) ? `~${p.slice(home.length)}` : p;
|
|
36
|
+
}
|
|
17
37
|
/** The picker's CLI half (owner 2026-09-08): a profile's LEGAL effort levels
|
|
18
38
|
* and its default, from the registry, shown wherever the profile is listed
|
|
19
39
|
* — kiso never silently drops a level, and now it shows the ones it takes.
|
|
20
40
|
* A model the registry does not know shows `unknown` (nothing is guessed);
|
|
21
41
|
* it must not read `none`, which is a REAL level since the Responses rows. */
|
|
42
|
+
/** A context figure for a NOTICE, in the status row's own form.
|
|
43
|
+
*
|
|
44
|
+
* `displayCtxRatio` returns NaN as its honest "nobody states this model's
|
|
45
|
+
* window", and the status row renders that as `ctx ?`. The `/compact`
|
|
46
|
+
* recap multiplied it and interpolated the result, so the same unknown
|
|
47
|
+
* reached the screen as `ctx NaN% → NaN%`: one value with a contract, and
|
|
48
|
+
* a second consumer that had not read it. The `%` moves inside, because
|
|
49
|
+
* the unknown form does not carry one. */
|
|
50
|
+
function ctxPercent(ratio) {
|
|
51
|
+
return Number.isFinite(ratio) ? `${Math.round(ratio * 100)}%` : "?";
|
|
52
|
+
}
|
|
22
53
|
function effortNote(p) {
|
|
23
54
|
const reasoning = lookupModelMetadata(p.model, p.baseUrl)?.capabilities.reasoning ?? null;
|
|
24
55
|
const effort = reasoning?.effort ?? null;
|
|
@@ -119,9 +150,21 @@ export function parseBang(trimmed) {
|
|
|
119
150
|
* No approval panel, at any tier including plan: the human typed the
|
|
120
151
|
* command. That is not a policy exemption, it is structural — this
|
|
121
152
|
* dispatcher only ever sees lines a human submitted, so a model cannot
|
|
122
|
-
* reach this branch.
|
|
153
|
+
* reach this branch.
|
|
154
|
+
*
|
|
155
|
+
* One check it does take: kiso never serves its own credential store to a
|
|
156
|
+
* model, and `!cmd` hands its output to one. `!!cmd` does not — but the
|
|
157
|
+
* two are one keystroke apart, so a line naming a protected file runs
|
|
158
|
+
* under neither, and the store stays one `cat` away in the person's own
|
|
159
|
+
* terminal. */
|
|
123
160
|
function runBang(command, send, ctx) {
|
|
124
161
|
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
162
|
+
const guard = protectedShellVerdict(command, codingToolOptions().workspaceRoot, protectedFiles(), { home: homedir(), kisoHome: kisoHome() });
|
|
163
|
+
if (guard.refused) {
|
|
164
|
+
bodyLog(protectedBangReason(guard));
|
|
165
|
+
ctx.input.prompt();
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
125
168
|
const controller = new AbortController();
|
|
126
169
|
activeBang = controller;
|
|
127
170
|
try {
|
|
@@ -458,7 +501,7 @@ export function dispatch(line, ctx) {
|
|
|
458
501
|
const picked = await new Promise((resolve) => {
|
|
459
502
|
ctx.input.panelAsk(modePickView({
|
|
460
503
|
header: `mode — current: ${current}`,
|
|
461
|
-
options:
|
|
504
|
+
options: OFFERED_MODES.map((name) => ({ label: name, note: [MODE_NOTE[name], ...(name === current ? ["current"] : [])].join(" · ") })),
|
|
462
505
|
},
|
|
463
506
|
// DC-12 (design §4): a panel WAITING ON A HUMAN says ❯.
|
|
464
507
|
ctx.isRunning() ? "❯ run paused" : `▸ ${current}`), (v) => resolve(v.action === "picked" ? v.result : null));
|
|
@@ -468,10 +511,10 @@ export function dispatch(line, ctx) {
|
|
|
468
511
|
ctx.input.prompt();
|
|
469
512
|
return; // esc — nothing switched, nothing said
|
|
470
513
|
}
|
|
471
|
-
const chosen = "index" in picked ?
|
|
514
|
+
const chosen = "index" in picked ? OFFERED_MODES[picked.index] : MODES.find((x) => x === picked.custom.trim());
|
|
472
515
|
if (chosen === undefined) {
|
|
473
516
|
bodyLog(`no such mode: ${"index" in picked ? String(picked.index) : picked.custom.trim()}`);
|
|
474
|
-
bodyLog(`tiers: ${
|
|
517
|
+
bodyLog(`tiers: ${OFFERED_MODES.join(" ")}`);
|
|
475
518
|
}
|
|
476
519
|
else {
|
|
477
520
|
setMode(chosen);
|
|
@@ -482,11 +525,11 @@ export function dispatch(line, ctx) {
|
|
|
482
525
|
return;
|
|
483
526
|
}
|
|
484
527
|
bodyLog(`mode ${getMode()}`);
|
|
485
|
-
bodyLog(`tiers: ${
|
|
528
|
+
bodyLog(`tiers: ${OFFERED_MODES.join(" ")}`);
|
|
486
529
|
}
|
|
487
530
|
else if (m === undefined) {
|
|
488
531
|
bodyLog(`no such mode: ${trimmed.slice(5).trim()}`);
|
|
489
|
-
bodyLog(`tiers: ${
|
|
532
|
+
bodyLog(`tiers: ${OFFERED_MODES.join(" ")}`);
|
|
490
533
|
}
|
|
491
534
|
else {
|
|
492
535
|
setMode(m);
|
|
@@ -655,11 +698,16 @@ export function dispatch(line, ctx) {
|
|
|
655
698
|
...(profile.baseUrl !== undefined ? { baseUrl: profile.baseUrl } : {}),
|
|
656
699
|
}),
|
|
657
700
|
},
|
|
701
|
+
// A1b: the in-run tiers are drawn from the same window.
|
|
702
|
+
contextWindow: contextWindowTokens({ model: profile.model, ...(profile.baseUrl !== undefined ? { baseUrl: profile.baseUrl } : {}) }),
|
|
658
703
|
// OR-1: the endpoint is the fourth passenger — the cost
|
|
659
704
|
// path and the window lookup key on (model, endpoint).
|
|
660
705
|
...(profile.baseUrl !== undefined ? { baseUrl: profile.baseUrl } : {}),
|
|
661
706
|
...(scope !== undefined ? { scope } : {}),
|
|
662
707
|
...(reasoning !== undefined ? { reasoning } : {}),
|
|
708
|
+
// 0.40.0: the NAME the person switched to, recorded for
|
|
709
|
+
// display — a direct provider/model has none.
|
|
710
|
+
profileName: direct === null ? profName : null,
|
|
663
711
|
};
|
|
664
712
|
ctx.session.setModelBinding(binding);
|
|
665
713
|
setLastBinding(binding);
|
|
@@ -721,7 +769,12 @@ export function dispatch(line, ctx) {
|
|
|
721
769
|
};
|
|
722
770
|
ctx.input.onEscape(onEscape);
|
|
723
771
|
let compactStart = 0;
|
|
724
|
-
let
|
|
772
|
+
let repaintCompacting = () => { };
|
|
773
|
+
// 0.40.0 (the owner's dogfood): the row walks the WORKING twinkle,
|
|
774
|
+
// the same spinner as a running turn — a static glyph repainted
|
|
775
|
+
// once a second read as a stalled screen while the model thought
|
|
776
|
+
let glyph = STATUS_GLYPHS[0];
|
|
777
|
+
let stopSpinner = null;
|
|
725
778
|
// the `as` on the initializer keeps the flow type the full union —
|
|
726
779
|
// onStart fills this during the call, but a closure assignment
|
|
727
780
|
// never re-narrows the outer scope (it would read `never`)
|
|
@@ -729,27 +782,51 @@ export function dispatch(line, ctx) {
|
|
|
729
782
|
// the ctx estimate BEFORE the summarized event lands (the used
|
|
730
783
|
// fraction — the recap's "ctx 91% → 34%" drops after compacting)
|
|
731
784
|
let ctxBefore = null;
|
|
785
|
+
// 0.40.0: the summary call's progress against its output budget —
|
|
786
|
+
// the latest report, read by the row; the runtime reports per
|
|
787
|
+
// streamed chunk and the row repaints at most every 200 ms
|
|
788
|
+
let progress = null;
|
|
789
|
+
let lastProgressPaint = 0;
|
|
732
790
|
const compacting = (info) => {
|
|
733
|
-
|
|
791
|
+
// 0.40.0: composed by the row seam, not a template here — the row
|
|
792
|
+
// is where the launch build's progress segment plugs in.
|
|
793
|
+
const text = (elapsed) => compactingStatus(glyph, info.rounds, info.tokens, elapsed, undefined, retryOnRow(), progress);
|
|
794
|
+
repaintCompacting = () => dock.setStatus(text(Math.round((Date.now() - compactStart) / 1000)), "esc to cancel");
|
|
734
795
|
compactStart = Date.now();
|
|
735
|
-
ctxBefore =
|
|
796
|
+
ctxBefore = ctxPercent(ctx.estimateCtx());
|
|
736
797
|
dock.setStatus(text(0), "esc to cancel");
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
798
|
+
stopSpinner = startStatusSpinner((next) => {
|
|
799
|
+
glyph = next;
|
|
800
|
+
repaintCompacting();
|
|
801
|
+
});
|
|
740
802
|
};
|
|
741
803
|
try {
|
|
742
804
|
// R3a: /compact <focus> — the words after the command steer
|
|
743
805
|
// the summary ("keep the auth details"); bare /compact is
|
|
744
806
|
// byte-identical to the pre-round call.
|
|
745
807
|
const focus = trimmed.slice(8).trim();
|
|
808
|
+
setRetryShown(null);
|
|
746
809
|
const result = await ctx.session.summarize({
|
|
747
810
|
signal: abort.signal,
|
|
811
|
+
// 0.39.2: the gesture gets the measured output budget and
|
|
812
|
+
// thinking off where the registry can vouch for turning it
|
|
813
|
+
// off. A fixed 4,000 was failing live sessions with "the
|
|
814
|
+
// summary turn ended with max_tokens"; a complete checkpoint
|
|
815
|
+
// of the reported size measured 18,837.
|
|
816
|
+
manualBudget: true,
|
|
748
817
|
...(focus !== "" ? { focus } : {}),
|
|
749
818
|
onStart: (info) => {
|
|
750
819
|
compactInfo = info;
|
|
751
820
|
compacting(info);
|
|
752
821
|
},
|
|
822
|
+
onProgress: (p) => {
|
|
823
|
+
progress = { produced: p.produced, budget: p.budget, reasoningUnseen: p.reasoningUnseen };
|
|
824
|
+
const now = Date.now();
|
|
825
|
+
if (now - lastProgressPaint < 200)
|
|
826
|
+
return;
|
|
827
|
+
lastProgressPaint = now;
|
|
828
|
+
repaintCompacting();
|
|
829
|
+
},
|
|
753
830
|
});
|
|
754
831
|
if (result === null) {
|
|
755
832
|
body.notice("[/compact] nothing to compact — fewer than 5 rounds yet");
|
|
@@ -760,11 +837,11 @@ export function dispatch(line, ctx) {
|
|
|
760
837
|
// rounds, the one summary, the savings, the ctx drop
|
|
761
838
|
// (the estimate BEFORE vs AFTER — the same chars/4
|
|
762
839
|
// proxy the status bar shows, marked ~), and the time.
|
|
763
|
-
const ctxAfter =
|
|
840
|
+
const ctxAfter = ctxPercent(ctx.estimateCtx());
|
|
764
841
|
const elapsed = compactStart > 0 ? settledLabel((Date.now() - compactStart) / 1000) : "?s";
|
|
765
842
|
// a non-null result implies onStart ran — the "?" is
|
|
766
843
|
// reachable only at the type level
|
|
767
|
-
body.notice(`[/compact] ✦ compacted · ${compactInfo?.rounds ?? "?"} rounds → 1 summary · saved ~${kUnit(result.savedTokens)} · ctx ${ctxBefore ?? "?"}
|
|
844
|
+
body.notice(`[/compact] ✦ compacted · ${compactInfo?.rounds ?? "?"} rounds → 1 summary · saved ~${kUnit(result.savedTokens)} · ctx ${ctxBefore ?? "?"} → ${ctxAfter} · ${elapsed}`);
|
|
768
845
|
// 0.39.1 — the boundary row, under the recap.
|
|
769
846
|
//
|
|
770
847
|
// `summarized` is appended OFF-LOOP: session.summarize()
|
|
@@ -811,8 +888,10 @@ export function dispatch(line, ctx) {
|
|
|
811
888
|
}
|
|
812
889
|
}
|
|
813
890
|
finally {
|
|
814
|
-
|
|
815
|
-
|
|
891
|
+
stopSpinner?.();
|
|
892
|
+
// ADR-0005 Amendment 2: a retry belongs to the call that
|
|
893
|
+
// announced it — none may outlive the compaction into the idle row.
|
|
894
|
+
setRetryShown(null);
|
|
816
895
|
ctx.paintIdle();
|
|
817
896
|
}
|
|
818
897
|
ctx.input.prompt();
|
|
@@ -893,8 +972,15 @@ export function dispatch(line, ctx) {
|
|
|
893
972
|
// would; the validation is the difference
|
|
894
973
|
if (!ctx.sessions().includes(arg)) {
|
|
895
974
|
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
896
|
-
|
|
897
|
-
|
|
975
|
+
const route = ctx.route?.(arg) ?? null;
|
|
976
|
+
if (route === null || route.kind === "refused") {
|
|
977
|
+
bodyLog(route?.line ?? `no such session: ${escapeTerminal(arg)} — /resume lists them`);
|
|
978
|
+
ctx.input.prompt();
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
if (route.kind === "elsewhere" && route.line !== null)
|
|
982
|
+
bodyLog(route.line);
|
|
983
|
+
ctx.requestSwitch(arg);
|
|
898
984
|
});
|
|
899
985
|
return;
|
|
900
986
|
}
|
|
@@ -916,6 +1002,16 @@ export function dispatch(line, ctx) {
|
|
|
916
1002
|
ctx.input.prompt(); // esc — nothing switched, nothing said
|
|
917
1003
|
return;
|
|
918
1004
|
}
|
|
1005
|
+
// 0.40.0: `tab` lists every project; another project's recorded
|
|
1006
|
+
// session is refused with where to go
|
|
1007
|
+
const route = ctx.sessions().includes(picked) ? null : (ctx.route?.(picked) ?? null);
|
|
1008
|
+
if (route?.kind === "refused") {
|
|
1009
|
+
bodyLog(route.line);
|
|
1010
|
+
ctx.input.prompt();
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
if (route?.kind === "elsewhere" && route.line !== null)
|
|
1014
|
+
bodyLog(route.line);
|
|
919
1015
|
ctx.requestSwitch(picked);
|
|
920
1016
|
});
|
|
921
1017
|
return;
|
|
@@ -928,6 +1024,25 @@ export function dispatch(line, ctx) {
|
|
|
928
1024
|
});
|
|
929
1025
|
return;
|
|
930
1026
|
}
|
|
1027
|
+
// 0.40.0 — a person's skill: `/skills`, `/skill <name> [args]`, or
|
|
1028
|
+
// `/<name> [args]` for a name no built-in above claimed. Placed AFTER
|
|
1029
|
+
// every built-in so a built-in always wins, and BEFORE the unknown-
|
|
1030
|
+
// command line so an installed skill is never called unknown.
|
|
1031
|
+
const skill = resolveSkillLine(trimmed, loadedSkillsCatalog(), slashCommandNames());
|
|
1032
|
+
if (trimmed === "/skills" || trimmed === "/skill" || skill !== null) {
|
|
1033
|
+
if (skill?.kind === "submit") {
|
|
1034
|
+
ctx.submitTurn(skill.content, skill.via);
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
1038
|
+
if (skill?.kind === "list")
|
|
1039
|
+
bodyLog(skillsRows(loadedSkillsCatalog(), skillSource, tildePath(join(kisoHome(), "skills"))).map(escapeTerminal).join("\n"));
|
|
1040
|
+
else if (skill?.kind === "error")
|
|
1041
|
+
bodyLog(escapeTerminal(skill.message));
|
|
1042
|
+
ctx.input.prompt();
|
|
1043
|
+
});
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
931
1046
|
// PH-1a (finding PH-F1): an unrecognized slash command is an ERROR,
|
|
932
1047
|
// never a turn — the fallthrough used to hand "/clear", "/exit", or a
|
|
933
1048
|
// typo to the model, burning a request on text the user meant as a
|
package/dist/floor.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 0.40.0 — the catastrophe floor (launch-weekend plan §4, minimum shape;
|
|
3
|
+
* owner, 2026-09-17: "bypass stays bypass: `rm -rf /tmp/probe` runs. The
|
|
4
|
+
* floor refuses only unrecoverable targets"; the lead's review and rulings
|
|
5
|
+
* of 2026-09-18).
|
|
6
|
+
*
|
|
7
|
+
* A destructive command whose target cannot be recovered is REFUSED, in
|
|
8
|
+
* every mode, bypass included, as a precondition — a chain member that
|
|
9
|
+
* denies, and a deny is what nothing outranks. The destructive commands:
|
|
10
|
+
* `rm`; `git clean -f`, `git reset --hard`, `git checkout -- <paths>` and
|
|
11
|
+
* the same loss spelled `git checkout .` / `-f`, `git restore` touching
|
|
12
|
+
* the worktree, `git switch -f` (R5); `find … -delete` with no selecting
|
|
13
|
+
* primary before it (R6). The unrecoverable targets:
|
|
14
|
+
*
|
|
15
|
+
* - `/`, the home directory, the workspace root or any directory above it;
|
|
16
|
+
* - the workspace's `.git` (R7);
|
|
17
|
+
* - `~/.ssh`, `~/.config`, `~/.kiso`, `~/.gnupg`, `~/.aws`, or inside one;
|
|
18
|
+
* - a system root and what is inside it — except the temp family and the
|
|
19
|
+
* software prefixes (/opt/homebrew, /usr/local, linuxbrew), where only
|
|
20
|
+
* the root itself is, and /Volumes /Users /home /mnt /media, where the
|
|
21
|
+
* root and its direct children are (R8). Inside the workspace or the
|
|
22
|
+
* home directory is never a system root's;
|
|
23
|
+
* - a wildcard over any of those (`~/*`, `/*`, `*` at the workspace root —
|
|
24
|
+
* a component of nothing but globs; `*.log` ranges over SOME entries);
|
|
25
|
+
* - a target that is only a variable (`$DIR/`, `"$X"/*`) — empty, it is
|
|
26
|
+
* the root of whatever comes after it. `$HOME` and `$PWD` are known.
|
|
27
|
+
*
|
|
28
|
+
* This is a reading of the command line, not a guarantee: the sandbox is
|
|
29
|
+
* the guarantee, and it comes after the launch. So the reading errs toward
|
|
30
|
+
* seeing a command — it looks inside `$( )`, backticks, `( )`, `sh -c` and
|
|
31
|
+
* `eval`, through `sudo`/`env`/`timeout`/`command` and shell keywords, and
|
|
32
|
+
* follows `cd` by its joiner — and a line that knocks the reader out is
|
|
33
|
+
* denied. Documented, not refused: `xargs rm`, script files and heredocs,
|
|
34
|
+
* `find -exec rm`, `mv`, `chmod -R`, `rmdir`, `unlink`, `truncate`,
|
|
35
|
+
* `shred`, `>` truncation, `rsync --delete`, `npx rimraf`, `trash`,
|
|
36
|
+
* `fish -c`, `ssh`, `docker`, `node -e` / `python -c` / `perl -e`, brace
|
|
37
|
+
* expansion, `$'…'`, `~+`, popd past one level, nesting past 16.
|
|
38
|
+
*
|
|
39
|
+
* The same resolver as the read-only allow decides where a path goes.
|
|
40
|
+
* Default on; `"floor": "off"` in the USER config turns it off, and a
|
|
41
|
+
* project config cannot — a repository must not be able to lower it.
|
|
42
|
+
*/
|
|
43
|
+
import type { PolicyCall } from "@vincemakes/kiso-core";
|
|
44
|
+
import type { KisoExtension } from "@vincemakes/kiso-runtime";
|
|
45
|
+
import { type LooseWord } from "./shell-words.js";
|
|
46
|
+
export type FloorVerdict = {
|
|
47
|
+
readonly refused: false;
|
|
48
|
+
} | {
|
|
49
|
+
readonly refused: true;
|
|
50
|
+
readonly why: string;
|
|
51
|
+
};
|
|
52
|
+
/** Past keywords, `sudo -u x`, `env A=1`, `timeout 5`, `command`, … to the
|
|
53
|
+
* command that runs. Matched by basename: `/usr/bin/env rm` is env. */
|
|
54
|
+
export declare function unwrap(argv: readonly LooseWord[]): readonly LooseWord[];
|
|
55
|
+
/** `sh -c '…'`, `bash -lc '…'`, `eval …`: the command lines inside.
|
|
56
|
+
* eval CONCATENATES its words with spaces and parses the result again —
|
|
57
|
+
* that is its meaning, and `eval 'rm -rf ~'` is read that way. It is also
|
|
58
|
+
* read once more with each word re-quoted, so `eval sh -c 'rm -rf /'`
|
|
59
|
+
* keeps its script whole (B10, the lead's reading): either refusing is a
|
|
60
|
+
* refusal. In the re-quoted reading `~` is already expanded and a word
|
|
61
|
+
* holding an unknown stays raw — its value is unknown to eval too. */
|
|
62
|
+
export declare function innerLines(argv: readonly LooseWord[], home: string): {
|
|
63
|
+
readonly lines: readonly string[];
|
|
64
|
+
readonly subshell: boolean;
|
|
65
|
+
} | null;
|
|
66
|
+
/**
|
|
67
|
+
* Would the floor refuse this command line? `workspaceRoot` is where the
|
|
68
|
+
* shell runs it.
|
|
69
|
+
*/
|
|
70
|
+
export declare function floorCheck(commandLine: string, workspaceRoot: string, home?: string): FloorVerdict;
|
|
71
|
+
/** Is there a destructive command anywhere in the line, whatever its
|
|
72
|
+
* target? A saved allow is never inherited by one (plan §4). */
|
|
73
|
+
export declare function isDestructive(commandLine: string): boolean;
|
|
74
|
+
/** The chain member, at the chain's head: DENY or ABSTAIN. */
|
|
75
|
+
export declare function floorExtension(on: () => boolean, workspaceRoot: () => string, check?: typeof floorCheck): KisoExtension;
|
|
76
|
+
/** Plan §4: "destructive commands never inherit a saved allow in the
|
|
77
|
+
* asking modes" — the predicate the saved-allow wrap
|
|
78
|
+
* (protected-writes.ts) is given, keyed on destructiveness, not on the
|
|
79
|
+
* target: a saved allow never runs even `rm -rf build` unasked. */
|
|
80
|
+
export declare function isDestructiveCall(call: PolicyCall): boolean;
|