@vincemakes/kiso-tools-node 0.1.29 → 0.1.31
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/dist/index.d.ts +3 -3
- package/dist/index.js +24 -24
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* explicit timeout and an output cap so a runaway command cannot flood the
|
|
12
12
|
* context.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* the token round: reads are RANGEABLE (read_file offset/limit, default head 200
|
|
15
15
|
* lines) and search/list are capped (50 / 200) — every truncation carries
|
|
16
16
|
* an actionable continuation note (deterministic per file state), so the
|
|
17
17
|
* model always has a path to the full content.
|
|
@@ -37,7 +37,7 @@ export declare class PathEscapeError extends Error {
|
|
|
37
37
|
}
|
|
38
38
|
export declare function resolveWithinRoot(root: string, input: string): string;
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
40
|
+
* round 10: the canonical path a tool will actually touch, as an ABSOLUTE path.
|
|
41
41
|
* Symlinks in the deepest EXISTING ancestor are resolved — a file to be
|
|
42
42
|
* created under a symlinked directory lands in the TARGET, not in the
|
|
43
43
|
* link — and the not-yet-existing tail is re-appended. Shared by the
|
|
@@ -49,7 +49,7 @@ export interface WorkspaceToolsOptions {
|
|
|
49
49
|
/** The workspace the tools may touch; everything else is refused. */
|
|
50
50
|
readonly workspaceRoot: string;
|
|
51
51
|
/**
|
|
52
|
-
*
|
|
52
|
+
* bootstrap #3 (finding #7): "inherit" keeps kiso's own provider credentials in
|
|
53
53
|
* the shell child's environment. DEFAULT (absent): the credentials are
|
|
54
54
|
* STRIPPED — a shell command must not inherit the agent's API keys (a
|
|
55
55
|
* nested kiso would hit the REAL provider and blow up faux e2e runs;
|
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* explicit timeout and an output cap so a runaway command cannot flood the
|
|
12
12
|
* context.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* the token round: reads are RANGEABLE (read_file offset/limit, default head 200
|
|
15
15
|
* lines) and search/list are capped (50 / 200) — every truncation carries
|
|
16
16
|
* an actionable continuation note (deterministic per file state), so the
|
|
17
17
|
* model always has a path to the full content.
|
|
@@ -22,7 +22,7 @@ import { basename, dirname, isAbsolute, join, relative, resolve } from "node:pat
|
|
|
22
22
|
import { defineTool } from "@vincemakes/kiso-core";
|
|
23
23
|
const OUTPUT_CAP = 100_000; // chars of output a tool result may carry
|
|
24
24
|
const DEFAULT_SHELL_TIMEOUT_MS = 30_000;
|
|
25
|
-
//
|
|
25
|
+
// the token round: the scoped-read defaults — read_file shows the head 200 lines
|
|
26
26
|
// of a large file (with an actionable continuation note, never a silent
|
|
27
27
|
// drop), search_text caps at 50 excerpts, list_dir at 200 entries. The
|
|
28
28
|
// red line: every truncation names its continuation — the model always
|
|
@@ -88,7 +88,7 @@ export function resolveWithinRoot(root, input) {
|
|
|
88
88
|
return canonical;
|
|
89
89
|
}
|
|
90
90
|
/**
|
|
91
|
-
*
|
|
91
|
+
* round 10: the canonical path a tool will actually touch, as an ABSOLUTE path.
|
|
92
92
|
* Symlinks in the deepest EXISTING ancestor are resolved — a file to be
|
|
93
93
|
* created under a symlinked directory lands in the TARGET, not in the
|
|
94
94
|
* link — and the not-yet-existing tail is re-appended. Shared by the
|
|
@@ -116,7 +116,7 @@ export function canonicalTargetPath(input) {
|
|
|
116
116
|
return missingTail.length > 0 ? join(ancestorReal, ...missingTail) : ancestorReal;
|
|
117
117
|
}
|
|
118
118
|
/**
|
|
119
|
-
* The inode-boundary policy for READS (
|
|
119
|
+
* The inode-boundary policy for READS (round 8): a hard link inside the workspace
|
|
120
120
|
* may point at an inode whose OTHER links live outside (e.g. /etc/passwd) —
|
|
121
121
|
* reading it would silently exfiltrate external content. Policy:
|
|
122
122
|
* - regular, single-link files: read;
|
|
@@ -132,13 +132,13 @@ function inodeReadPolicy(root, full) {
|
|
|
132
132
|
return `not a regular file — refusing to read (${full})`;
|
|
133
133
|
if (st.nlink <= 1)
|
|
134
134
|
return null;
|
|
135
|
-
//
|
|
135
|
+
// round 4: the link count is verified STRUCTURALLY, never by counting
|
|
136
136
|
// newline-split text. `find -print0` emits NUL-separated paths — a file
|
|
137
137
|
// named "inside\nspoof" is ONE path, not two — and every match is then
|
|
138
138
|
// re-statted and checked for the EXACT dev+ino pair (an inode number
|
|
139
139
|
// alone is not identity across devices). Any failure to verify every
|
|
140
140
|
// link is fail-closed: the file is refused.
|
|
141
|
-
//
|
|
141
|
+
// round 5(P2-3): the workspace root is CANONICALIZED before the scan —
|
|
142
142
|
// find on a symlinked root would not follow the symlink into the real
|
|
143
143
|
// tree, undercounting the in-workspace links and misjudging a legal
|
|
144
144
|
// hard link as an external escape.
|
|
@@ -335,8 +335,8 @@ export function searchTextTool(opts) {
|
|
|
335
335
|
}
|
|
336
336
|
else if (entry.isFile()) {
|
|
337
337
|
try {
|
|
338
|
-
//
|
|
339
|
-
// to an external inode is not searched.
|
|
338
|
+
// round 8: same inode boundary as read_file — a hard link
|
|
339
|
+
// to an external inode is not searched. round 4 (adversarial):
|
|
340
340
|
// the link count is verified against the WORKSPACE
|
|
341
341
|
// root, not the search subroot — a link that lives
|
|
342
342
|
// inside the workspace but outside the search dir is
|
|
@@ -397,12 +397,12 @@ export function writeFileTool(opts) {
|
|
|
397
397
|
const tmp = `${full}.kiso-tmp-${process.pid}-${crypto.randomUUID()}`;
|
|
398
398
|
let preservedMode;
|
|
399
399
|
try {
|
|
400
|
-
// E
|
|
400
|
+
// E group: SAFE REPLACEMENT — write a temp file next to the
|
|
401
401
|
// target and rename it over the directory entry. A hard link
|
|
402
402
|
// inside the workspace that shares an EXTERNAL inode is
|
|
403
403
|
// therefore never overwritten: rename replaces the entry,
|
|
404
404
|
// not the shared inode.
|
|
405
|
-
//
|
|
405
|
+
// round 8: an existing file keeps its mode — a 0755 script stays
|
|
406
406
|
// 0755 after replacement (rename drops the temp's default
|
|
407
407
|
// mode, so it is copied onto the temp first).
|
|
408
408
|
if (existsSync(full))
|
|
@@ -422,7 +422,7 @@ export function writeFileTool(opts) {
|
|
|
422
422
|
return { content: `wrote ${path} (${content.length} chars)`, isError: false };
|
|
423
423
|
}
|
|
424
424
|
catch (err) {
|
|
425
|
-
//
|
|
425
|
+
// round 8: a failed write never leaves a temp file with the FULL
|
|
426
426
|
// content behind — it is unlinked in every failure path.
|
|
427
427
|
try {
|
|
428
428
|
unlinkSync(tmp);
|
|
@@ -468,8 +468,8 @@ export function editFileTool(opts) {
|
|
|
468
468
|
if (index === -1) {
|
|
469
469
|
return { content: `edit_file: pattern not found in ${path}`, isError: true, errorKind: "invalid_input" };
|
|
470
470
|
}
|
|
471
|
-
// E
|
|
472
|
-
//
|
|
471
|
+
// E group: safe replacement — never rewrite a shared external inode via a hard link.
|
|
472
|
+
// round 8: the edited file keeps its mode.
|
|
473
473
|
preservedMode = statSync(full).mode & 0o7777;
|
|
474
474
|
writeFileSync(tmp, text.slice(0, index) + replace + text.slice(index + search.length), "utf8");
|
|
475
475
|
chmodSync(tmp, preservedMode);
|
|
@@ -481,7 +481,7 @@ export function editFileTool(opts) {
|
|
|
481
481
|
return { content: `edited ${path}`, isError: false };
|
|
482
482
|
}
|
|
483
483
|
catch (err) {
|
|
484
|
-
//
|
|
484
|
+
// round 8: a failed edit never leaves a temp file behind.
|
|
485
485
|
try {
|
|
486
486
|
unlinkSync(tmp);
|
|
487
487
|
}
|
|
@@ -494,7 +494,7 @@ export function editFileTool(opts) {
|
|
|
494
494
|
});
|
|
495
495
|
}
|
|
496
496
|
/**
|
|
497
|
-
*
|
|
497
|
+
* bootstrap #3 (finding #7): the explicit credential list stripped from shell
|
|
498
498
|
* children — the agent's own provider surface (both families' keys, base
|
|
499
499
|
* URLs, and model choices) plus the generic API-key / auth-token patterns
|
|
500
500
|
* that cover other providers. Everything else in the environment passes
|
|
@@ -534,7 +534,7 @@ export function shellTool(opts) {
|
|
|
534
534
|
},
|
|
535
535
|
execute: async ({ command, timeoutMs }, ctx) => {
|
|
536
536
|
const timeout = timeoutMs ?? DEFAULT_SHELL_TIMEOUT_MS;
|
|
537
|
-
// E
|
|
537
|
+
// E group: a PRE-aborted signal never spawns the command.
|
|
538
538
|
if (ctx.signal.aborted) {
|
|
539
539
|
return { content: "shell aborted before start", isError: true, errorKind: "fatal" };
|
|
540
540
|
}
|
|
@@ -542,7 +542,7 @@ export function shellTool(opts) {
|
|
|
542
542
|
// detached: the command gets its OWN process group, so a
|
|
543
543
|
// timeout/abort can kill the WHOLE TREE (children included),
|
|
544
544
|
// not just the outer shell (Area 4). cwd is the workspace.
|
|
545
|
-
//
|
|
545
|
+
// bootstrap #3 (finding #7): the shell child NEVER inherits kiso's own
|
|
546
546
|
// provider credentials by default — only the explicit
|
|
547
547
|
// shellEnv: "inherit" opt-in keeps them.
|
|
548
548
|
const child = spawn(command, {
|
|
@@ -562,13 +562,13 @@ export function shellTool(opts) {
|
|
|
562
562
|
return;
|
|
563
563
|
settled = true;
|
|
564
564
|
clearTimeout(timer);
|
|
565
|
-
// E
|
|
565
|
+
// E group: the abort listener is removed once settled — it
|
|
566
566
|
// must not accumulate across runs.
|
|
567
567
|
ctx.signal.removeEventListener("abort", onAbort);
|
|
568
568
|
resolvePromise(result);
|
|
569
569
|
};
|
|
570
570
|
/**
|
|
571
|
-
* Kill the whole tree and CONFIRM it exited (
|
|
571
|
+
* Kill the whole tree and CONFIRM it exited (rounds 8/11):
|
|
572
572
|
*
|
|
573
573
|
* 1. FREEZE the root (SIGSTOP) FIRST — a stopped shell cannot
|
|
574
574
|
* fork new descendants while we enumerate;
|
|
@@ -586,7 +586,7 @@ export function shellTool(opts) {
|
|
|
586
586
|
*/
|
|
587
587
|
const killTree = () => new Promise((resolveKill) => {
|
|
588
588
|
const tracked = new Set();
|
|
589
|
-
//
|
|
589
|
+
// round 11 (adversarial): the ROOT itself is tracked too — the
|
|
590
590
|
// verdict must not read "aborted" while the root
|
|
591
591
|
// survives. DOCUMENTED LIMITS: (1) a process that
|
|
592
592
|
// forks between SIGSTOP delivery and the next scan,
|
|
@@ -628,7 +628,7 @@ export function shellTool(opts) {
|
|
|
628
628
|
}
|
|
629
629
|
previous = current;
|
|
630
630
|
}
|
|
631
|
-
// The process group (E
|
|
631
|
+
// The process group (E group: never kill an undefined/0
|
|
632
632
|
// pid), which also takes the frozen root down.
|
|
633
633
|
if (child.pid !== undefined && child.pid > 0) {
|
|
634
634
|
try {
|
|
@@ -683,7 +683,7 @@ export function shellTool(opts) {
|
|
|
683
683
|
: { content: `exit ${code}: ${combined}`, isError: true, errorKind: "fatal" });
|
|
684
684
|
});
|
|
685
685
|
// The kernel's abort reaches the command AND its whole tree.
|
|
686
|
-
// The listener is removed by settle (E
|
|
686
|
+
// The listener is removed by settle (E group).
|
|
687
687
|
const uncertainVerdict = (unconfirmed) => unconfirmed.length > 0
|
|
688
688
|
? `could not confirm ${unconfirmed.length} descendant(s) exited (pids ${unconfirmed.join(", ")}) — treat the side effect as UNCERTAIN`
|
|
689
689
|
: "";
|
|
@@ -711,7 +711,7 @@ export function shellTool(opts) {
|
|
|
711
711
|
}
|
|
712
712
|
/**
|
|
713
713
|
* All live pids whose ancestor chain includes `pid`, from the pid table
|
|
714
|
-
* (
|
|
714
|
+
* (round 8: `ps -axo pid=,ppid=` — the ONLY way to see a setsid()-escaped
|
|
715
715
|
* process, which is in its own group and invisible to a group kill).
|
|
716
716
|
*/
|
|
717
717
|
function descendantsOf(pid) {
|
|
@@ -749,7 +749,7 @@ function descendantsOf(pid) {
|
|
|
749
749
|
/**
|
|
750
750
|
* Poll the pid table until NONE of the tracked pids is alive (bounded).
|
|
751
751
|
* Returns the pids still alive at the deadline — the caller MUST NOT
|
|
752
|
-
* report "aborted"/"timed out" while any tracked pid survives (
|
|
752
|
+
* report "aborted"/"timed out" while any tracked pid survives (round 11).
|
|
753
753
|
*/
|
|
754
754
|
function waitAllDead(pids) {
|
|
755
755
|
if (pids.length === 0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tools-node",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.31",
|
|
4
4
|
"description": "kiso coding tools for Node hosts — read file, list directory, search text, write/edit file, shell command.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"test": "vitest run"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@vincemakes/kiso-core": "0.1.
|
|
24
|
+
"@vincemakes/kiso-core": "0.1.30"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@types/node": "^26.1.2",
|