@datename/fastcode-termux 0.1.6-beta → 0.1.8-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +4 -0
- package/README.md +50 -17
- package/dist/agent/approval-json.d.ts +43 -0
- package/dist/agent/approval-json.js +69 -0
- package/dist/agent/approval-native.d.ts +3 -0
- package/dist/agent/approval-native.js +165 -0
- package/dist/agent/approval-store.d.ts +16 -0
- package/dist/agent/approval-store.js +21 -0
- package/dist/agent/approval.d.ts +33 -0
- package/dist/agent/approval.js +40 -5
- package/dist/agent/compaction.d.ts +2 -0
- package/dist/agent/compaction.js +60 -17
- package/dist/agent/guards.d.ts +16 -16
- package/dist/agent/guards.js +978 -24
- package/dist/agent/policy/defaults.d.ts +16 -0
- package/dist/agent/policy/defaults.js +17 -0
- package/dist/agent/policy/egress.d.ts +32 -0
- package/dist/agent/policy/egress.js +280 -0
- package/dist/agent/process-tree.d.ts +20 -0
- package/dist/agent/process-tree.js +81 -10
- package/dist/agent/protocol.d.ts +20 -1
- package/dist/agent/protocol.js +165 -38
- package/dist/agent/registry.d.ts +3 -1
- package/dist/agent/registry.js +168 -11
- package/dist/agent/runner.d.ts +21 -0
- package/dist/agent/runner.js +44 -5
- package/dist/agent/shell-ast.d.ts +11 -0
- package/dist/agent/shell-ast.js +204 -0
- package/dist/agent/shell-session.d.ts +14 -0
- package/dist/agent/shell-session.js +251 -40
- package/dist/agent/tool-schemas.js +26 -7
- package/dist/agent/tools/append_file.js +3 -1
- package/dist/agent/tools/apply_patch.js +44 -4
- package/dist/agent/tools/copy_file.js +26 -3
- package/dist/agent/tools/delete_file.js +22 -3
- package/dist/agent/tools/grep.js +35 -4
- package/dist/agent/tools/make_dir.js +3 -1
- package/dist/agent/tools/move_path.d.ts +7 -0
- package/dist/agent/tools/move_path.js +31 -5
- package/dist/agent/tools/read_file.js +63 -3
- package/dist/agent/tools/read_many.js +21 -3
- package/dist/agent/tools/readers.d.ts +53 -1
- package/dist/agent/tools/readers.js +236 -26
- package/dist/agent/tools/replace_in_file.js +38 -5
- package/dist/agent/tools/shell.js +2 -2
- package/dist/agent/tools/types.d.ts +26 -0
- package/dist/agent/tools/validate.d.ts +15 -2
- package/dist/agent/tools/validate.js +66 -3
- package/dist/agent/tools/write_file.js +3 -1
- package/dist/app.d.ts +2 -1
- package/dist/app.js +526 -85
- package/dist/cli-main.js +363 -0
- package/dist/cli.js +397 -337
- package/dist/commands/about.js +1 -1
- package/dist/commands/clear.d.ts +1 -0
- package/dist/commands/clear.js +1 -1
- package/dist/commands/compact.d.ts +12 -0
- package/dist/commands/compact.js +14 -2
- package/dist/commands/context.js +5 -4
- package/dist/commands/index.js +2 -0
- package/dist/commands/jobs.js +3 -2
- package/dist/commands/kill.js +3 -1
- package/dist/commands/login.d.ts +2 -2
- package/dist/commands/login.js +15 -15
- package/dist/commands/model.d.ts +1 -13
- package/dist/commands/model.js +6 -38
- package/dist/commands/provider.d.ts +1 -15
- package/dist/commands/provider.js +4 -22
- package/dist/commands/skills.js +34 -4
- package/dist/commands/threads.d.ts +9 -0
- package/dist/commands/threads.js +26 -0
- package/dist/config/store.d.ts +21 -0
- package/dist/config/store.js +47 -9
- package/dist/config/trust.d.ts +9 -0
- package/dist/config/trust.js +41 -2
- package/dist/headless.d.ts +7 -0
- package/dist/headless.js +213 -0
- package/dist/index.js +19 -3
- package/dist/lib/debug.d.ts +2 -0
- package/dist/lib/debug.js +22 -0
- package/dist/lib/diagnostics.d.ts +19 -0
- package/dist/lib/diagnostics.js +32 -6
- package/dist/lib/highlight-subset.d.ts +21 -0
- package/dist/lib/highlight-subset.js +88 -0
- package/dist/lib/lifecycle.d.ts +5 -0
- package/dist/lib/lifecycle.js +62 -0
- package/dist/lib/markdown.js +22 -4
- package/dist/providers/deepseek.js +1 -1
- package/dist/providers/openaiCompatible.d.ts +24 -2
- package/dist/providers/openaiCompatible.js +20 -6
- package/dist/providers/spec.d.ts +12 -0
- package/dist/providers/spec.js +29 -0
- package/dist/session/log.d.ts +14 -2
- package/dist/session/log.js +56 -27
- package/dist/session/projection.d.ts +34 -1
- package/dist/session/projection.js +315 -46
- package/dist/skills/manager.d.ts +15 -0
- package/dist/skills/manager.js +135 -18
- package/dist/types.d.ts +15 -0
- package/dist/ui/StatusBar.js +1 -1
- package/package.json +3 -2
- package/src/agent/approval-json.ts +103 -0
- package/src/agent/approval-native.ts +168 -0
- package/src/agent/approval-store.ts +38 -0
- package/src/agent/approval.ts +58 -11
- package/src/agent/compaction.ts +62 -17
- package/src/agent/guards.ts +1049 -25
- package/src/agent/policy/defaults.ts +34 -0
- package/src/agent/policy/egress.ts +335 -0
- package/src/agent/process-tree.ts +105 -23
- package/src/agent/protocol.ts +187 -39
- package/src/agent/registry.ts +186 -14
- package/src/agent/runner.ts +50 -7
- package/src/agent/shell-ast.ts +227 -0
- package/src/agent/shell-session.ts +265 -41
- package/src/agent/tool-schemas.ts +26 -7
- package/src/agent/tools/append_file.ts +3 -1
- package/src/agent/tools/apply_patch.ts +45 -4
- package/src/agent/tools/copy_file.ts +27 -3
- package/src/agent/tools/delete_file.ts +22 -3
- package/src/agent/tools/grep.ts +36 -4
- package/src/agent/tools/make_dir.ts +3 -1
- package/src/agent/tools/move_path.ts +38 -5
- package/src/agent/tools/read_file.ts +64 -3
- package/src/agent/tools/read_many.ts +21 -3
- package/src/agent/tools/readers.ts +289 -26
- package/src/agent/tools/replace_in_file.ts +39 -5
- package/src/agent/tools/shell.ts +2 -2
- package/src/agent/tools/types.ts +43 -1
- package/src/agent/tools/validate.ts +73 -4
- package/src/agent/tools/write_file.ts +3 -1
- package/src/app.ts +533 -82
- package/src/{cli.ts → cli-main.ts} +34 -6
- package/src/commands/about.ts +1 -1
- package/src/commands/clear.ts +1 -1
- package/src/commands/compact.ts +14 -2
- package/src/commands/context.ts +5 -4
- package/src/commands/index.ts +2 -0
- package/src/commands/jobs.ts +3 -2
- package/src/commands/kill.ts +3 -1
- package/src/commands/login.ts +15 -15
- package/src/commands/model.ts +6 -38
- package/src/commands/provider.ts +4 -22
- package/src/commands/skills.ts +34 -4
- package/src/commands/threads.ts +28 -0
- package/src/config/store.ts +47 -9
- package/src/config/trust.ts +45 -2
- package/src/headless.ts +225 -0
- package/src/index.ts +19 -3
- package/src/lib/debug.ts +24 -0
- package/src/lib/diagnostics.ts +32 -6
- package/src/lib/highlight-subset.ts +93 -0
- package/src/lib/lifecycle.ts +69 -0
- package/src/lib/markdown.ts +22 -4
- package/src/providers/deepseek.ts +1 -1
- package/src/providers/openaiCompatible.ts +47 -10
- package/src/providers/spec.ts +42 -0
- package/src/session/log.ts +70 -27
- package/src/session/projection.ts +360 -47
- package/src/skills/manager.ts +140 -18
- package/src/types/highlight-shims.d.ts +14 -0
- package/src/types.ts +16 -1
- package/src/ui/StatusBar.ts +1 -1
- package/tsconfig.json +1 -0
- /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight egress defaults. Kept separate from the policy engine so
|
|
3
|
+
* config/store can include the default object without loading guards.
|
|
4
|
+
*/
|
|
5
|
+
export type EgressMode = "off" | "audit" | "block";
|
|
6
|
+
export interface EgressConfig {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
mode: EgressMode;
|
|
9
|
+
denyPaths: string[];
|
|
10
|
+
denyGlobs: string[];
|
|
11
|
+
denyContentMarkers: string[];
|
|
12
|
+
maxScanBytes: number;
|
|
13
|
+
scanToolOutput: boolean;
|
|
14
|
+
}
|
|
15
|
+
export declare const DEFAULT_CONTENT_MARKERS: string[];
|
|
16
|
+
export declare const DEFAULT_EGRESS_CONFIG: EgressConfig;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const DEFAULT_CONTENT_MARKERS = [
|
|
2
|
+
"-----BEGIN [A-Z ]*PRIVATE KEY-----",
|
|
3
|
+
"AKIA[0-9A-Z]{16}",
|
|
4
|
+
"gh[pousr]_[A-Za-z0-9]{36,}",
|
|
5
|
+
"xox[baprs]-[A-Za-z0-9-]{10,}",
|
|
6
|
+
"AIza[0-9A-Za-z_-]{35}",
|
|
7
|
+
"sk-[A-Za-z0-9]{32,}",
|
|
8
|
+
];
|
|
9
|
+
export const DEFAULT_EGRESS_CONFIG = {
|
|
10
|
+
enabled: false,
|
|
11
|
+
mode: "audit",
|
|
12
|
+
denyPaths: [],
|
|
13
|
+
denyGlobs: [],
|
|
14
|
+
denyContentMarkers: [...DEFAULT_CONTENT_MARKERS],
|
|
15
|
+
maxScanBytes: 1_000_000,
|
|
16
|
+
scanToolOutput: true,
|
|
17
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { EgressAudit, ToolCall } from "../tools/types.js";
|
|
2
|
+
import { type EgressConfig, type EgressMode } from "./defaults.js";
|
|
3
|
+
export type { EgressConfig, EgressMode } from "./defaults.js";
|
|
4
|
+
export { DEFAULT_CONTENT_MARKERS, DEFAULT_EGRESS_CONFIG } from "./defaults.js";
|
|
5
|
+
export type EgressAction = "read" | "write" | "delete" | "egress" | "payload";
|
|
6
|
+
export interface EgressSubject {
|
|
7
|
+
outlet: string;
|
|
8
|
+
action: EgressAction;
|
|
9
|
+
target?: string;
|
|
10
|
+
payload?: string;
|
|
11
|
+
outbound?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export interface EgressDecision {
|
|
14
|
+
outlet: string;
|
|
15
|
+
target?: string;
|
|
16
|
+
rule: string;
|
|
17
|
+
mode: EgressMode;
|
|
18
|
+
wouldBlock: boolean;
|
|
19
|
+
code: "EGRESS_DENY";
|
|
20
|
+
reason: string;
|
|
21
|
+
}
|
|
22
|
+
export interface EgressEvaluation {
|
|
23
|
+
action: "allow" | "audit" | "deny";
|
|
24
|
+
decision?: EgressDecision;
|
|
25
|
+
}
|
|
26
|
+
export declare function normalizeEgressConfig(raw: any): EgressConfig;
|
|
27
|
+
export declare function egressSubjects(call: ToolCall): EgressSubject[];
|
|
28
|
+
export declare function evaluateEgressCall(call: ToolCall, cwd: string, config: EgressConfig): EgressEvaluation;
|
|
29
|
+
export declare function evaluateEgressOutput(tool: string, output: string, config: EgressConfig): EgressEvaluation;
|
|
30
|
+
export declare function egressAudit(decision: EgressDecision): EgressAudit;
|
|
31
|
+
export declare function formatEgressDenial(decision: EgressDecision): string;
|
|
32
|
+
export declare function formatEgressAudit(audit: EgressAudit): string;
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Application-layer egress policy.
|
|
3
|
+
*
|
|
4
|
+
* This is intentionally a pure policy module plus a thin registry hook. It is
|
|
5
|
+
* not a kernel and cannot stop code that bypasses FastCode's tool registry.
|
|
6
|
+
*
|
|
7
|
+
* Default posture: audit only (`enabled:false`, `mode:"audit"`). Set
|
|
8
|
+
* `egress.enabled=true` and `egress.mode="block"` to enforce.
|
|
9
|
+
*/
|
|
10
|
+
import { basename, relative, resolve } from "node:path";
|
|
11
|
+
import { isCriticalReadTarget, isSensitiveTarget, isSharedStoragePath } from "../guards.js";
|
|
12
|
+
import { DEFAULT_CONTENT_MARKERS, DEFAULT_EGRESS_CONFIG } from "./defaults.js";
|
|
13
|
+
export { DEFAULT_CONTENT_MARKERS, DEFAULT_EGRESS_CONFIG } from "./defaults.js";
|
|
14
|
+
function strings(value) {
|
|
15
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === "string" && entry.trim()).map((entry) => entry.trim()) : [];
|
|
16
|
+
}
|
|
17
|
+
function positiveInt(value, fallback) {
|
|
18
|
+
const number = Number(value);
|
|
19
|
+
return Number.isFinite(number) && number > 0 ? Math.floor(number) : fallback;
|
|
20
|
+
}
|
|
21
|
+
export function normalizeEgressConfig(raw) {
|
|
22
|
+
const source = raw && typeof raw === "object" ? raw : {};
|
|
23
|
+
const mode = source.mode === "block" ? "block" : source.mode === "off" ? "off" : "audit";
|
|
24
|
+
return {
|
|
25
|
+
enabled: source.enabled === true,
|
|
26
|
+
mode,
|
|
27
|
+
denyPaths: strings(source.denyPaths),
|
|
28
|
+
denyGlobs: strings(source.denyGlobs),
|
|
29
|
+
// Built-in markers cannot be removed by configuration.
|
|
30
|
+
denyContentMarkers: Array.from(new Set([...DEFAULT_CONTENT_MARKERS, ...strings(source.denyContentMarkers)])),
|
|
31
|
+
maxScanBytes: positiveInt(source.maxScanBytes, DEFAULT_EGRESS_CONFIG.maxScanBytes),
|
|
32
|
+
scanToolOutput: source.scanToolOutput !== false,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
const compileCache = new Map();
|
|
36
|
+
function safeRegExp(pattern) {
|
|
37
|
+
try {
|
|
38
|
+
return new RegExp(pattern, "i");
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
function globToRegExp(glob) {
|
|
45
|
+
let source = "^";
|
|
46
|
+
for (let index = 0; index < glob.length; index += 1) {
|
|
47
|
+
const char = glob[index];
|
|
48
|
+
if (char === "*") {
|
|
49
|
+
if (glob[index + 1] === "*") {
|
|
50
|
+
source += ".*";
|
|
51
|
+
index += 1;
|
|
52
|
+
if (glob[index + 1] === "/")
|
|
53
|
+
index += 1;
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
source += "[^/]*";
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (char === "?") {
|
|
60
|
+
source += "[^/]";
|
|
61
|
+
}
|
|
62
|
+
else if (/[.+^${}()|[\]\\]/.test(char)) {
|
|
63
|
+
source += "\\" + char;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
source += char;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return new RegExp(source + "$", "i");
|
|
70
|
+
}
|
|
71
|
+
function compile(config) {
|
|
72
|
+
const key = JSON.stringify(config);
|
|
73
|
+
const cached = compileCache.get(key);
|
|
74
|
+
if (cached)
|
|
75
|
+
return cached;
|
|
76
|
+
const compiled = {
|
|
77
|
+
markers: config.denyContentMarkers.map(safeRegExp).filter(Boolean),
|
|
78
|
+
globs: config.denyGlobs.map(globToRegExp),
|
|
79
|
+
denyPaths: config.denyPaths.map((entry) => resolve(entry).replace(/\\/g, "/")),
|
|
80
|
+
};
|
|
81
|
+
compileCache.set(key, compiled);
|
|
82
|
+
if (compileCache.size > 8)
|
|
83
|
+
compileCache.delete(compileCache.keys().next().value);
|
|
84
|
+
return compiled;
|
|
85
|
+
}
|
|
86
|
+
function expandHome(target) {
|
|
87
|
+
const home = process.env["HOME"] || process.env["USERPROFILE"] || "";
|
|
88
|
+
if (!home)
|
|
89
|
+
return target;
|
|
90
|
+
if (target === "~")
|
|
91
|
+
return home;
|
|
92
|
+
if (target.startsWith("~/") || target.startsWith("~\\"))
|
|
93
|
+
return home + target.slice(1);
|
|
94
|
+
return target.replace(/^\$(?:HOME|\{HOME\})/i, home);
|
|
95
|
+
}
|
|
96
|
+
function normalizeTarget(target, cwd) {
|
|
97
|
+
return resolve(cwd, expandHome(target)).replace(/\\/g, "/");
|
|
98
|
+
}
|
|
99
|
+
function isInside(child, parent) {
|
|
100
|
+
const rel = relative(parent, child).replace(/\\/g, "/");
|
|
101
|
+
return rel === "" || (!rel.startsWith("..") && !rel.startsWith("/"));
|
|
102
|
+
}
|
|
103
|
+
function markerHit(payload, compiled) {
|
|
104
|
+
for (const marker of compiled.markers) {
|
|
105
|
+
if (marker.test(payload))
|
|
106
|
+
return marker.source;
|
|
107
|
+
}
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
function scanWindow(payload, maxBytes) {
|
|
111
|
+
if (Buffer.byteLength(payload, "utf8") <= maxBytes)
|
|
112
|
+
return payload;
|
|
113
|
+
const buffer = Buffer.from(payload, "utf8");
|
|
114
|
+
const half = Math.max(1, Math.floor(maxBytes / 2));
|
|
115
|
+
return `${buffer.subarray(0, half).toString("utf8")}\n${buffer.subarray(buffer.length - half).toString("utf8")}`;
|
|
116
|
+
}
|
|
117
|
+
function evaluateSubject(subject, cwd, config, compiled) {
|
|
118
|
+
const decide = (rule, reason) => ({
|
|
119
|
+
outlet: subject.outlet,
|
|
120
|
+
target: subject.target,
|
|
121
|
+
rule,
|
|
122
|
+
mode: config.mode,
|
|
123
|
+
wouldBlock: true,
|
|
124
|
+
code: "EGRESS_DENY",
|
|
125
|
+
reason,
|
|
126
|
+
});
|
|
127
|
+
if (subject.action === "payload") {
|
|
128
|
+
const payload = subject.payload ?? "";
|
|
129
|
+
if (!payload)
|
|
130
|
+
return undefined;
|
|
131
|
+
const overBudget = Buffer.byteLength(payload, "utf8") > config.maxScanBytes;
|
|
132
|
+
if (overBudget && subject.outbound)
|
|
133
|
+
return decide("scan_budget_exceeded", `payload exceeds the ${config.maxScanBytes}-byte egress scan budget`);
|
|
134
|
+
const marker = markerHit(scanWindow(payload, config.maxScanBytes), compiled);
|
|
135
|
+
if (marker)
|
|
136
|
+
return decide("content_marker", `payload matched denyContentMarkers (${marker})`);
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
const target = subject.target;
|
|
140
|
+
if (!target)
|
|
141
|
+
return undefined;
|
|
142
|
+
if (subject.action === "read" && isCriticalReadTarget(target))
|
|
143
|
+
return decide("critical_credential_read", "reading a critical private key or system credential file");
|
|
144
|
+
if (subject.action !== "read" && isSensitiveTarget(target))
|
|
145
|
+
return decide("sensitive_path", "writing or deleting a protected credential/session file");
|
|
146
|
+
if (subject.action !== "read" && isSharedStoragePath(target))
|
|
147
|
+
return decide("shared_storage", "writing to shared storage is treated as an egress surface");
|
|
148
|
+
const normalized = normalizeTarget(target, cwd);
|
|
149
|
+
for (const denyPath of compiled.denyPaths) {
|
|
150
|
+
if (isInside(normalized, denyPath))
|
|
151
|
+
return decide("config:deny_path", `path matches denyPaths entry ${denyPath}`);
|
|
152
|
+
}
|
|
153
|
+
for (const glob of compiled.globs) {
|
|
154
|
+
if (glob.test(normalized) || glob.test(basename(normalized)))
|
|
155
|
+
return decide("config:deny_glob", "path matches a denyGlobs entry");
|
|
156
|
+
}
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
function pathSubject(outlet, action, target) {
|
|
160
|
+
return typeof target === "string" && target ? { outlet, action, target } : undefined;
|
|
161
|
+
}
|
|
162
|
+
function patchTargets(patch) {
|
|
163
|
+
const subjects = [];
|
|
164
|
+
const filePattern = /^\*\*\* (Add|Update|Delete) File: (.+)$/gm;
|
|
165
|
+
for (const match of patch.matchAll(filePattern)) {
|
|
166
|
+
const action = match[1] === "Delete" ? "delete" : "write";
|
|
167
|
+
subjects.push({ outlet: "apply_patch.path", action, target: match[2].trim() });
|
|
168
|
+
}
|
|
169
|
+
const movePattern = /^\*\*\* Move to: (.+)$/gm;
|
|
170
|
+
for (const match of patch.matchAll(movePattern))
|
|
171
|
+
subjects.push({ outlet: "apply_patch.move", action: "write", target: match[1].trim() });
|
|
172
|
+
return subjects;
|
|
173
|
+
}
|
|
174
|
+
export function egressSubjects(call) {
|
|
175
|
+
const subjects = [];
|
|
176
|
+
const add = (subject) => {
|
|
177
|
+
if (subject)
|
|
178
|
+
subjects.push(subject);
|
|
179
|
+
};
|
|
180
|
+
switch (call.tool) {
|
|
181
|
+
case "read_file":
|
|
182
|
+
case "list_files":
|
|
183
|
+
case "stat_path":
|
|
184
|
+
case "grep":
|
|
185
|
+
add(pathSubject(`${call.tool}.path`, "read", typeof call.path === "string" ? call.path : "."));
|
|
186
|
+
break;
|
|
187
|
+
case "read_many":
|
|
188
|
+
if (Array.isArray(call.paths)) {
|
|
189
|
+
for (const path of call.paths)
|
|
190
|
+
add(pathSubject("read_many.paths", "read", path));
|
|
191
|
+
}
|
|
192
|
+
break;
|
|
193
|
+
case "write_file":
|
|
194
|
+
case "append_file":
|
|
195
|
+
add(pathSubject(`${call.tool}.path`, "write", call.path));
|
|
196
|
+
if (typeof call.content === "string")
|
|
197
|
+
subjects.push({ outlet: `${call.tool}.content`, action: "payload", payload: call.content, outbound: false });
|
|
198
|
+
break;
|
|
199
|
+
case "replace_in_file":
|
|
200
|
+
add(pathSubject("replace_in_file.path", "write", call.path));
|
|
201
|
+
if (typeof call.replace === "string")
|
|
202
|
+
subjects.push({ outlet: "replace_in_file.replace", action: "payload", payload: call.replace, outbound: false });
|
|
203
|
+
break;
|
|
204
|
+
case "delete_file":
|
|
205
|
+
add(pathSubject("delete_file.path", "delete", call.path));
|
|
206
|
+
break;
|
|
207
|
+
case "make_dir":
|
|
208
|
+
add(pathSubject("make_dir.path", "write", call.path));
|
|
209
|
+
break;
|
|
210
|
+
case "copy_file":
|
|
211
|
+
add(pathSubject("copy_file.from", "egress", call.from));
|
|
212
|
+
add(pathSubject("copy_file.to", "write", call.to));
|
|
213
|
+
break;
|
|
214
|
+
case "move_path":
|
|
215
|
+
add(pathSubject("move_path.from", "egress", call.from));
|
|
216
|
+
add(pathSubject("move_path.to", "write", call.to));
|
|
217
|
+
break;
|
|
218
|
+
case "apply_patch":
|
|
219
|
+
if (typeof call.patch === "string") {
|
|
220
|
+
subjects.push({ outlet: "apply_patch.patch", action: "payload", payload: call.patch, outbound: false });
|
|
221
|
+
subjects.push(...patchTargets(call.patch));
|
|
222
|
+
}
|
|
223
|
+
break;
|
|
224
|
+
case "shell":
|
|
225
|
+
if (typeof call.command === "string")
|
|
226
|
+
subjects.push({ outlet: "shell.command", action: "payload", payload: call.command, outbound: true });
|
|
227
|
+
break;
|
|
228
|
+
default:
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
return subjects;
|
|
232
|
+
}
|
|
233
|
+
export function evaluateEgressCall(call, cwd, config) {
|
|
234
|
+
if (config.mode === "off")
|
|
235
|
+
return { action: "allow" };
|
|
236
|
+
const compiled = compile(config);
|
|
237
|
+
for (const subject of egressSubjects(call)) {
|
|
238
|
+
const decision = evaluateSubject(subject, cwd, config, compiled);
|
|
239
|
+
if (!decision)
|
|
240
|
+
continue;
|
|
241
|
+
const enforce = config.enabled && config.mode === "block";
|
|
242
|
+
return { action: enforce ? "deny" : "audit", decision };
|
|
243
|
+
}
|
|
244
|
+
return { action: "allow" };
|
|
245
|
+
}
|
|
246
|
+
export function evaluateEgressOutput(tool, output, config) {
|
|
247
|
+
if (!config.scanToolOutput || config.mode === "off" || !output)
|
|
248
|
+
return { action: "allow" };
|
|
249
|
+
const compiled = compile(config);
|
|
250
|
+
const decision = evaluateSubject({
|
|
251
|
+
outlet: `${tool}.output`,
|
|
252
|
+
action: "payload",
|
|
253
|
+
payload: output,
|
|
254
|
+
outbound: true,
|
|
255
|
+
}, process.cwd(), config, compiled);
|
|
256
|
+
if (!decision)
|
|
257
|
+
return { action: "allow" };
|
|
258
|
+
const enforce = config.enabled && config.mode === "block";
|
|
259
|
+
return { action: enforce ? "deny" : "audit", decision };
|
|
260
|
+
}
|
|
261
|
+
export function egressAudit(decision) {
|
|
262
|
+
return {
|
|
263
|
+
outlet: decision.outlet,
|
|
264
|
+
target: decision.target,
|
|
265
|
+
rule: decision.rule,
|
|
266
|
+
mode: decision.mode === "block" ? "block" : "audit",
|
|
267
|
+
wouldBlock: decision.wouldBlock,
|
|
268
|
+
code: decision.code,
|
|
269
|
+
reason: decision.reason,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
export function formatEgressDenial(decision) {
|
|
273
|
+
const target = decision.target ? ` target=${JSON.stringify(decision.target)}` : "";
|
|
274
|
+
return `[egress] DENY rule=${decision.rule}${target} reason=${decision.reason}\n` +
|
|
275
|
+
"This decision cannot be overridden by FASTCODE_ALLOW_DANGEROUS, trust, sharedDirs, or session/always approval.";
|
|
276
|
+
}
|
|
277
|
+
export function formatEgressAudit(audit) {
|
|
278
|
+
const target = audit.target ? ` target=${JSON.stringify(audit.target)}` : "";
|
|
279
|
+
return `[egress] AUDIT rule=${audit.rule}${target} would_block=${audit.wouldBlock} reason=${audit.reason}`;
|
|
280
|
+
}
|
|
@@ -1,2 +1,22 @@
|
|
|
1
1
|
import { type ChildProcess } from "node:child_process";
|
|
2
|
+
export declare const KILL_GRACE_MS = 1000;
|
|
3
|
+
export interface TerminateOptions {
|
|
4
|
+
/** Milliseconds to wait after SIGTERM before escalating to SIGKILL. */
|
|
5
|
+
graceMs?: number;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Send a single signal to the process group (falling back to the pid).
|
|
9
|
+
* Kept for callers that need one-shot signalling; shell sessions should use
|
|
10
|
+
* terminateProcessTree so SIGKILL escalation is always available.
|
|
11
|
+
*/
|
|
2
12
|
export declare function killProcessTree(child: ChildProcess | undefined, signal?: NodeJS.Signals): void;
|
|
13
|
+
/**
|
|
14
|
+
* Terminate a spawned process group with a SIGTERM -> grace -> SIGKILL flow.
|
|
15
|
+
*
|
|
16
|
+
* Boundary: this only guarantees the spawned process group is killed. A
|
|
17
|
+
* descendant that calls setsid() leaves the group and may escape; we do not
|
|
18
|
+
* scan for ppid=1 processes because that risks killing unrelated background
|
|
19
|
+
* jobs on Termux. Callers should report
|
|
20
|
+
* "process group killed; setsid descendants may escape".
|
|
21
|
+
*/
|
|
22
|
+
export declare function terminateProcessTree(child: ChildProcess | undefined, options?: TerminateOptions): void;
|
|
@@ -1,4 +1,52 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
export const KILL_GRACE_MS = 1000;
|
|
3
|
+
const pendingKills = new WeakMap();
|
|
4
|
+
function sendSignal(pid, signal) {
|
|
5
|
+
try {
|
|
6
|
+
process.kill(-pid, signal);
|
|
7
|
+
return "sent";
|
|
8
|
+
}
|
|
9
|
+
catch (err) {
|
|
10
|
+
if (err?.code !== "ESRCH")
|
|
11
|
+
return "sent";
|
|
12
|
+
try {
|
|
13
|
+
process.kill(pid, signal);
|
|
14
|
+
return "sent";
|
|
15
|
+
}
|
|
16
|
+
catch (err2) {
|
|
17
|
+
return err2?.code === "ESRCH" ? "gone" : "sent";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function isAlive(pid) {
|
|
22
|
+
try {
|
|
23
|
+
process.kill(-pid, 0);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
if (err?.code !== "ESRCH")
|
|
28
|
+
return true;
|
|
29
|
+
try {
|
|
30
|
+
process.kill(pid, 0);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
catch (err2) {
|
|
34
|
+
return err2?.code !== "ESRCH";
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function clearPending(child) {
|
|
39
|
+
const timer = pendingKills.get(child);
|
|
40
|
+
if (timer) {
|
|
41
|
+
clearTimeout(timer);
|
|
42
|
+
pendingKills.delete(child);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Send a single signal to the process group (falling back to the pid).
|
|
47
|
+
* Kept for callers that need one-shot signalling; shell sessions should use
|
|
48
|
+
* terminateProcessTree so SIGKILL escalation is always available.
|
|
49
|
+
*/
|
|
2
50
|
export function killProcessTree(child, signal = "SIGTERM") {
|
|
3
51
|
if (!child || typeof child.pid !== "number")
|
|
4
52
|
return;
|
|
@@ -17,15 +65,38 @@ export function killProcessTree(child, signal = "SIGTERM") {
|
|
|
17
65
|
}
|
|
18
66
|
return;
|
|
19
67
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
68
|
+
sendSignal(child.pid, signal);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Terminate a spawned process group with a SIGTERM -> grace -> SIGKILL flow.
|
|
72
|
+
*
|
|
73
|
+
* Boundary: this only guarantees the spawned process group is killed. A
|
|
74
|
+
* descendant that calls setsid() leaves the group and may escape; we do not
|
|
75
|
+
* scan for ppid=1 processes because that risks killing unrelated background
|
|
76
|
+
* jobs on Termux. Callers should report
|
|
77
|
+
* "process group killed; setsid descendants may escape".
|
|
78
|
+
*/
|
|
79
|
+
export function terminateProcessTree(child, options = {}) {
|
|
80
|
+
if (!child || typeof child.pid !== "number")
|
|
81
|
+
return;
|
|
82
|
+
if (process.platform === "win32") {
|
|
83
|
+
killProcessTree(child, "SIGTERM");
|
|
84
|
+
return;
|
|
30
85
|
}
|
|
86
|
+
const pid = child.pid;
|
|
87
|
+
clearPending(child);
|
|
88
|
+
if (sendSignal(pid, "SIGTERM") === "gone")
|
|
89
|
+
return;
|
|
90
|
+
const graceMs = Math.max(0, Math.floor(options.graceMs ?? KILL_GRACE_MS));
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
pendingKills.delete(child);
|
|
93
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
94
|
+
return;
|
|
95
|
+
if (!isAlive(pid))
|
|
96
|
+
return;
|
|
97
|
+
sendSignal(pid, "SIGKILL");
|
|
98
|
+
}, graceMs);
|
|
99
|
+
timer.unref?.();
|
|
100
|
+
pendingKills.set(child, timer);
|
|
101
|
+
child.once("exit", () => clearPending(child));
|
|
31
102
|
}
|
package/dist/agent/protocol.d.ts
CHANGED
|
@@ -1,4 +1,23 @@
|
|
|
1
|
-
export
|
|
1
|
+
export interface ToolCallRecord {
|
|
2
|
+
tool: string;
|
|
3
|
+
[key: string]: any;
|
|
4
|
+
}
|
|
5
|
+
export interface ToolCallParseResult {
|
|
6
|
+
calls: ToolCallRecord[];
|
|
7
|
+
/** End offset of the last complete construct that was consumed. */
|
|
8
|
+
consumedEnd: number;
|
|
9
|
+
/** True parse failures. Salvaged calls do not add an error. */
|
|
10
|
+
errors: string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Parse all complete tool calls, then salvage only the *remaining* text.
|
|
14
|
+
*
|
|
15
|
+
* Invariants (regression-tested):
|
|
16
|
+
* - a complete construct is consumed exactly once and never rescanned;
|
|
17
|
+
* - the salvage pass receives `raw.slice(consumedEnd)`, never the full text;
|
|
18
|
+
* - two adjacent normal calls plus a stray close tag stays at two calls.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseToolCalls(text: any): ToolCallParseResult;
|
|
2
21
|
export declare function stripToolCalls(text: any): any;
|
|
3
22
|
export declare function formatToolResults(results: any): string;
|
|
4
23
|
export declare const AGENT_SYSTEM_PROMPT: string;
|