@tpsdev-ai/agent 0.2.0 → 0.4.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 +54 -0
- package/dist/bin.d.ts +3 -0
- package/dist/bin.d.ts.map +1 -0
- package/dist/bin.js +32 -0
- package/dist/bin.js.map +1 -0
- package/dist/config.d.ts +23 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +39 -0
- package/dist/config.js.map +1 -0
- package/dist/governance/boundary.d.ts +30 -0
- package/dist/governance/boundary.d.ts.map +1 -0
- package/dist/governance/boundary.js +120 -0
- package/dist/governance/boundary.js.map +1 -0
- package/dist/governance/review-gate.d.ts +9 -0
- package/dist/governance/review-gate.d.ts.map +1 -0
- package/dist/governance/review-gate.js +28 -0
- package/dist/governance/review-gate.js.map +1 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/io/context.d.ts +18 -0
- package/dist/io/context.d.ts.map +1 -0
- package/dist/io/context.js +76 -0
- package/dist/io/context.js.map +1 -0
- package/dist/io/mail.d.ts +22 -0
- package/dist/io/mail.d.ts.map +1 -0
- package/dist/io/mail.js +45 -0
- package/dist/io/mail.js.map +1 -0
- package/dist/io/memory.d.ts +24 -0
- package/dist/io/memory.d.ts.map +1 -0
- package/dist/io/memory.js +91 -0
- package/dist/io/memory.js.map +1 -0
- package/dist/llm/provider.d.ts +26 -0
- package/dist/llm/provider.d.ts.map +1 -0
- package/dist/llm/provider.js +254 -0
- package/dist/llm/provider.js.map +1 -0
- package/dist/runtime/agent.d.ts +14 -0
- package/dist/runtime/agent.d.ts.map +1 -0
- package/dist/runtime/agent.js +46 -0
- package/dist/runtime/agent.js.map +1 -0
- package/dist/runtime/event-loop.d.ts +32 -0
- package/dist/runtime/event-loop.d.ts.map +1 -0
- package/dist/runtime/event-loop.js +178 -0
- package/dist/runtime/event-loop.js.map +1 -0
- package/dist/runtime/types.d.ts +66 -0
- package/dist/runtime/types.d.ts.map +1 -0
- package/dist/runtime/types.js +2 -0
- package/dist/runtime/types.js.map +1 -0
- package/dist/tools/edit.d.ts +4 -0
- package/dist/tools/edit.d.ts.map +1 -0
- package/dist/tools/edit.js +46 -0
- package/dist/tools/edit.js.map +1 -0
- package/dist/tools/exec.d.ts +4 -0
- package/dist/tools/exec.d.ts.map +1 -0
- package/dist/tools/exec.js +74 -0
- package/dist/tools/exec.js.map +1 -0
- package/dist/tools/index.d.ts +17 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +23 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/mail.d.ts +4 -0
- package/dist/tools/mail.d.ts.map +1 -0
- package/dist/tools/mail.js +19 -0
- package/dist/tools/mail.js.map +1 -0
- package/dist/tools/read.d.ts +4 -0
- package/dist/tools/read.d.ts.map +1 -0
- package/dist/tools/read.js +31 -0
- package/dist/tools/read.js.map +1 -0
- package/dist/tools/registry.d.ts +24 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/registry.js +23 -0
- package/dist/tools/registry.js.map +1 -0
- package/dist/tools/write.d.ts +4 -0
- package/dist/tools/write.d.ts.map +1 -0
- package/dist/tools/write.js +32 -0
- package/dist/tools/write.js.map +1 -0
- package/package.json +19 -4
- package/src/bin.ts +40 -0
- package/src/config.ts +64 -0
- package/src/governance/boundary.ts +112 -18
- package/src/index.ts +15 -2
- package/src/io/context.ts +59 -15
- package/src/io/memory.ts +53 -9
- package/src/llm/provider.ts +203 -42
- package/src/runtime/agent.ts +30 -3
- package/src/runtime/event-loop.ts +168 -26
- package/src/runtime/types.ts +54 -6
- package/src/tools/edit.ts +59 -0
- package/src/tools/exec.ts +92 -0
- package/src/tools/index.ts +30 -0
- package/src/tools/mail.ts +28 -0
- package/src/tools/read.ts +38 -0
- package/src/tools/registry.ts +16 -7
- package/src/tools/write.ts +40 -0
- package/src/types/js-tiktoken.d.ts +7 -0
- package/test/governance.test.ts +61 -32
- package/test/io.test.ts +15 -7
- package/test/runtime.test.ts +26 -5
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { BoundaryManager } from "../governance/boundary.js";
|
|
4
|
+
export function makeWriteTool(boundary) {
|
|
5
|
+
return {
|
|
6
|
+
name: "write",
|
|
7
|
+
description: "Create or overwrite a file.\nInput: {\"path\": string, \"content\": string}",
|
|
8
|
+
input_schema: {
|
|
9
|
+
path: { type: "string", description: "Path to write, relative to workspace" },
|
|
10
|
+
content: { type: "string", description: "File contents" },
|
|
11
|
+
},
|
|
12
|
+
async execute(args) {
|
|
13
|
+
const payload = args;
|
|
14
|
+
if (typeof payload.path !== "string" || typeof payload.content !== "string") {
|
|
15
|
+
return { content: "write tool requires path and content strings", isError: true };
|
|
16
|
+
}
|
|
17
|
+
const workspacePath = boundary.resolveWorkspacePath(payload.path);
|
|
18
|
+
if (existsSync(workspacePath) && !BoundaryManager.canFollowSymlink(workspacePath)) {
|
|
19
|
+
return { content: `Refusing to follow symlink: ${payload.path}`, isError: true };
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
mkdirSync(dirname(workspacePath), { recursive: true });
|
|
23
|
+
writeFileSync(workspacePath, payload.content, "utf-8");
|
|
24
|
+
return { content: `Wrote ${payload.path}`, isError: false };
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
return { content: `write failed: ${err?.message ?? String(err)}`, isError: true };
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
//# sourceMappingURL=write.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"write.js","sourceRoot":"","sources":["../../src/tools/write.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC;AAS5D,MAAM,UAAU,aAAa,CAAC,QAAyB;IACrD,OAAO;QACL,IAAI,EAAE,OAAO;QACb,WAAW,EAAE,6EAA6E;QAC1F,YAAY,EAAE;YACZ,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,sCAAsC,EAAE;YAC7E,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,eAAe,EAAE;SAC1D;QACD,KAAK,CAAC,OAAO,CAAC,IAA6B;YACzC,MAAM,OAAO,GAAI,IAA6B,CAAC;YAC/C,IAAI,OAAO,OAAO,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,OAAO,CAAC,OAAO,KAAK,QAAQ,EAAE,CAAC;gBAC5E,OAAO,EAAE,OAAO,EAAE,8CAA8C,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YACpF,CAAC;YAED,MAAM,aAAa,GAAG,QAAQ,CAAC,oBAAoB,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAClE,IAAI,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC,eAAe,CAAC,gBAAgB,CAAC,aAAa,CAAC,EAAE,CAAC;gBAClF,OAAO,EAAE,OAAO,EAAE,+BAA+B,OAAO,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YACnF,CAAC;YAED,IAAI,CAAC;gBACH,SAAS,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBACvD,aAAa,CAAC,aAAa,EAAE,OAAO,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;gBACvD,OAAO,EAAE,OAAO,EAAE,SAAS,OAAO,CAAC,IAAI,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YAC9D,CAAC;YAAC,OAAO,GAAQ,EAAE,CAAC;gBAClB,OAAO,EAAE,OAAO,EAAE,iBAAiB,GAAG,EAAE,OAAO,IAAI,MAAM,CAAC,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC;YACpF,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tpsdev-ai/agent",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Native TPS Agent Runtime
|
|
3
|
+
"version": "0.4.1",
|
|
4
|
+
"description": "Native TPS Agent Runtime \u2014 headless, mail-driven, nono-sandboxed",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -9,8 +9,15 @@
|
|
|
9
9
|
".": {
|
|
10
10
|
"import": "./dist/index.js",
|
|
11
11
|
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./bin": {
|
|
14
|
+
"import": "./dist/bin.js",
|
|
15
|
+
"types": "./dist/bin.d.ts"
|
|
12
16
|
}
|
|
13
17
|
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"tps-agent": "./dist/bin.js"
|
|
20
|
+
},
|
|
14
21
|
"scripts": {
|
|
15
22
|
"build": "tsc",
|
|
16
23
|
"dev": "tsc --watch",
|
|
@@ -18,10 +25,16 @@
|
|
|
18
25
|
"lint": "biome lint ./src",
|
|
19
26
|
"lint:ci": "biome lint ./src --max-diagnostics=200"
|
|
20
27
|
},
|
|
21
|
-
"keywords": [
|
|
28
|
+
"keywords": [
|
|
29
|
+
"agents",
|
|
30
|
+
"tps",
|
|
31
|
+
"runtime",
|
|
32
|
+
"mail-driven"
|
|
33
|
+
],
|
|
22
34
|
"license": "Apache-2.0",
|
|
23
35
|
"dependencies": {
|
|
24
36
|
"js-yaml": "^4.1.0",
|
|
37
|
+
"js-tiktoken": "^1.0.13",
|
|
25
38
|
"zod": "^3.24.0"
|
|
26
39
|
},
|
|
27
40
|
"devDependencies": {
|
|
@@ -31,5 +44,7 @@
|
|
|
31
44
|
"typescript": "^5.7.0"
|
|
32
45
|
},
|
|
33
46
|
"author": "tpsdev-ai",
|
|
34
|
-
"publishConfig": {
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
}
|
|
35
50
|
}
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { AgentRuntime } from "./runtime/agent.js";
|
|
3
|
+
import { loadAgentConfig } from "./config.js";
|
|
4
|
+
|
|
5
|
+
function usage(): never {
|
|
6
|
+
console.error("Usage: tps-agent run --config <agent.yaml> --message <text>");
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
async function main() {
|
|
11
|
+
const args = process.argv.slice(2);
|
|
12
|
+
const cmd = args[0];
|
|
13
|
+
|
|
14
|
+
if (cmd !== "run") {
|
|
15
|
+
usage();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const configIdx = args.indexOf("--config");
|
|
19
|
+
const msgIdx = args.indexOf("--message");
|
|
20
|
+
|
|
21
|
+
if (configIdx < 0 || msgIdx < 0) {
|
|
22
|
+
usage();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const configPath = args[configIdx + 1];
|
|
26
|
+
const message = args.slice(msgIdx + 1).join(" ");
|
|
27
|
+
|
|
28
|
+
if (!configPath || !message) {
|
|
29
|
+
usage();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const config = loadAgentConfig(configPath);
|
|
33
|
+
const runtime = new AgentRuntime(config);
|
|
34
|
+
await runtime.runOnce(message);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
main().catch((err) => {
|
|
38
|
+
console.error(err?.message || err);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import yaml from "js-yaml";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import type { AgentConfig } from "./runtime/types.js";
|
|
5
|
+
|
|
6
|
+
export interface AgentRuntimeConfig {
|
|
7
|
+
agentId: string;
|
|
8
|
+
name: string;
|
|
9
|
+
mailDir: string;
|
|
10
|
+
memoryPath?: string;
|
|
11
|
+
workspace: string;
|
|
12
|
+
systemPrompt?: string;
|
|
13
|
+
llm: {
|
|
14
|
+
provider: "anthropic" | "google" | "openai" | "ollama";
|
|
15
|
+
model: string;
|
|
16
|
+
apiKey?: string;
|
|
17
|
+
baseUrl?: string;
|
|
18
|
+
};
|
|
19
|
+
contextWindowTokens?: number;
|
|
20
|
+
maxTokens?: number;
|
|
21
|
+
tools?: Array<"read" | "write" | "edit" | "exec" | "mail">;
|
|
22
|
+
execAllowlist?: string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function loadAgentConfig(path: string): AgentConfig {
|
|
26
|
+
const raw = readFileSync(path, "utf-8");
|
|
27
|
+
const parsed = (yaml.load(raw) ?? {}) as Record<string, any>;
|
|
28
|
+
|
|
29
|
+
const workspace = String(parsed.workspace || parsed.repo || process.cwd());
|
|
30
|
+
const memoryPath = parsed.memoryPath || `${workspace}/.openclaw/agent.memory.jsonl`;
|
|
31
|
+
|
|
32
|
+
const agent: AgentConfig = {
|
|
33
|
+
agentId: String(parsed.agentId || parsed.id || "agent"),
|
|
34
|
+
name: String(parsed.name || parsed.agentId || "agent"),
|
|
35
|
+
workspace,
|
|
36
|
+
mailDir: String(parsed.mailDir || `${workspace}/mail`),
|
|
37
|
+
memoryPath: String(memoryPath),
|
|
38
|
+
systemPrompt: parsed.systemPrompt ? String(parsed.systemPrompt) : undefined,
|
|
39
|
+
contextWindowTokens: parsed.contextWindowTokens ? Number(parsed.contextWindowTokens) : 8192,
|
|
40
|
+
maxTokens: parsed.maxTokens ? Number(parsed.maxTokens) : 1024,
|
|
41
|
+
tools: parsed.tools ?? ["read", "write", "edit", "exec", "mail"],
|
|
42
|
+
execAllowlist: parsed.execAllowlist,
|
|
43
|
+
llm: {
|
|
44
|
+
provider: parsed.llm?.provider || parsed.provider || "openai",
|
|
45
|
+
model: parsed.llm?.model || parsed.model || "gpt-4o-mini",
|
|
46
|
+
apiKey: parsed.llm?.apiKey || parsed.apiKey,
|
|
47
|
+
baseUrl: parsed.llm?.baseUrl || parsed.baseUrl,
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return agent;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function defaultMemoryPath(workspace: string): string {
|
|
55
|
+
const digest = createHash("sha1").update(workspace).digest("hex").slice(0, 8);
|
|
56
|
+
return `${workspace}/.openclaw/memory-${digest}.jsonl`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function normalizeConfigObject(value: unknown): AgentRuntimeConfig {
|
|
60
|
+
if (typeof value !== "object" || !value) {
|
|
61
|
+
throw new Error("Invalid agent config");
|
|
62
|
+
}
|
|
63
|
+
return value as AgentRuntimeConfig;
|
|
64
|
+
}
|
|
@@ -1,37 +1,131 @@
|
|
|
1
|
+
import { resolve, relative, isAbsolute, sep } from "node:path";
|
|
2
|
+
import { accessSync, constants, existsSync, lstatSync, realpathSync } from "node:fs";
|
|
3
|
+
|
|
1
4
|
/**
|
|
2
|
-
* BoundaryManager
|
|
3
|
-
* allowed capabilities to the ToolRegistry.
|
|
4
|
-
*
|
|
5
|
-
* NOTE: The system prompt is NOT a security boundary. Hard nono syscall/network
|
|
6
|
-
* filters and pre-execution checks in ToolRegistry are the actual enforcement.
|
|
5
|
+
* BoundaryManager handles execution-time policy controls for security boundaries.
|
|
7
6
|
*/
|
|
8
7
|
export class BoundaryManager {
|
|
9
|
-
private allowedNetworkHosts
|
|
10
|
-
|
|
8
|
+
private allowedNetworkHosts = new Set<string>();
|
|
9
|
+
|
|
10
|
+
constructor(private readonly workspace: string) {}
|
|
11
11
|
|
|
12
12
|
addNetworkHost(host: string): void {
|
|
13
13
|
this.allowedNetworkHosts.add(host);
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
addPath(path: string): void {
|
|
17
|
-
this.allowedPaths.add(path);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
16
|
isNetworkAllowed(host: string): boolean {
|
|
21
17
|
return this.allowedNetworkHosts.has(host) || this.allowedNetworkHosts.has("*");
|
|
22
18
|
}
|
|
23
19
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
20
|
+
/**
|
|
21
|
+
* Resolve a user requested filesystem path against workspace and ensure
|
|
22
|
+
* it cannot escape the workspace root.
|
|
23
|
+
*/
|
|
24
|
+
resolveWorkspacePath(relativeOrAbsolutePath: string): string {
|
|
25
|
+
const target = resolve(this.workspace, relativeOrAbsolutePath);
|
|
26
|
+
const workspaceReal = realpathSync(this.workspace);
|
|
27
|
+
|
|
28
|
+
let normalized = target;
|
|
29
|
+
if (existsSync(target)) {
|
|
30
|
+
normalized = realpathSync(target);
|
|
31
|
+
} else {
|
|
32
|
+
// If target does not exist, resolve relative to existing parent.
|
|
33
|
+
const parent = resolve(target, "..");
|
|
34
|
+
if (existsSync(parent)) {
|
|
35
|
+
normalized = resolve(realpathSync(parent), target.slice(parent.length + 1));
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (!this.isWithinWorkspace(normalized, workspaceReal)) {
|
|
40
|
+
throw new Error(`Path traversal blocked: ${relativeOrAbsolutePath}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return target;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
private isWithinWorkspace(candidate: string, workspaceReal: string): boolean {
|
|
47
|
+
const rel = relative(workspaceReal, candidate);
|
|
48
|
+
if (rel === "") return true;
|
|
49
|
+
if (rel === ".." || rel.startsWith(`..${sep}`)) return false;
|
|
50
|
+
return !isAbsolute(rel);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Verify that a command is safe before exec.
|
|
55
|
+
*/
|
|
56
|
+
validateCommand(command: string, args: string[]): void {
|
|
57
|
+
if (!command) {
|
|
58
|
+
throw new Error("Exec requires a command");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const blockedFlags = ["--exec-path", "-e", "--eval", "-p", "-c", "/dev/fd", "--noprofile", "--norc", "node_options", "command="];
|
|
62
|
+
const tokens = [command, ...args].map((token) => String(token).toLowerCase());
|
|
63
|
+
|
|
64
|
+
for (const token of tokens) {
|
|
65
|
+
if (blockedFlags.includes(token) || token.includes("core.pager")) {
|
|
66
|
+
throw new Error(`Disallowed exec argument: ${token}`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Disallow attempts to execute compound expressions.
|
|
71
|
+
const full = tokens.join(" ");
|
|
72
|
+
if (full.includes("||") || full.includes("&&") || full.includes(";") || full.includes("|") || full.includes("$") || full.includes("`") ) {
|
|
73
|
+
throw new Error(`Disallowed exec argument: ${full}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Child processes must not inherit runtime secrets from the parent process.
|
|
79
|
+
*/
|
|
80
|
+
scrubEnvironment(extraKeys: string[] = []): NodeJS.ProcessEnv {
|
|
81
|
+
const env = { ...process.env } as NodeJS.ProcessEnv;
|
|
82
|
+
const denyPattern = /(API_KEY|APISECRET|SECRET|TOKEN|PASS|CREDENTIAL|AUTH)/i;
|
|
83
|
+
|
|
84
|
+
for (const key of Object.keys(env)) {
|
|
85
|
+
if (denyPattern.test(key)) {
|
|
86
|
+
delete env[key];
|
|
87
|
+
}
|
|
27
88
|
}
|
|
28
|
-
|
|
89
|
+
|
|
90
|
+
for (const key of extraKeys) {
|
|
91
|
+
if (key in env) {
|
|
92
|
+
delete env[key];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return env;
|
|
29
97
|
}
|
|
30
98
|
|
|
31
|
-
/** Returns
|
|
99
|
+
/** Returns human-readable boundaries for system prompts and audit logs. */
|
|
32
100
|
describeCapabilities(): string {
|
|
33
101
|
const nets = [...this.allowedNetworkHosts].join(", ") || "none";
|
|
34
|
-
|
|
35
|
-
|
|
102
|
+
return `Network access: ${nets}\nFilesystem access: ${this.workspace}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
static isFileReadable(path: string): boolean {
|
|
106
|
+
try {
|
|
107
|
+
accessSync(path, constants.R_OK);
|
|
108
|
+
return true;
|
|
109
|
+
} catch {
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
static isFileWritable(path: string): boolean {
|
|
115
|
+
try {
|
|
116
|
+
accessSync(path, constants.W_OK);
|
|
117
|
+
return true;
|
|
118
|
+
} catch {
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
static canFollowSymlink(path: string): boolean {
|
|
124
|
+
try {
|
|
125
|
+
const stat = lstatSync(path);
|
|
126
|
+
return !stat.isSymbolicLink();
|
|
127
|
+
} catch {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
36
130
|
}
|
|
37
131
|
}
|
package/src/index.ts
CHANGED
|
@@ -3,7 +3,17 @@
|
|
|
3
3
|
// Runtime
|
|
4
4
|
export { AgentRuntime } from "./runtime/agent.js";
|
|
5
5
|
export { EventLoop } from "./runtime/event-loop.js";
|
|
6
|
-
export type {
|
|
6
|
+
export type {
|
|
7
|
+
AgentConfig,
|
|
8
|
+
LLMConfig,
|
|
9
|
+
AgentState,
|
|
10
|
+
CompletionRequest,
|
|
11
|
+
CompletionResponse,
|
|
12
|
+
ToolCall,
|
|
13
|
+
ToolSpec,
|
|
14
|
+
LLMMessage,
|
|
15
|
+
ToolResult,
|
|
16
|
+
} from "./runtime/types.js";
|
|
7
17
|
|
|
8
18
|
// I/O
|
|
9
19
|
export { MailClient } from "./io/mail.js";
|
|
@@ -14,12 +24,15 @@ export { ContextManager } from "./io/context.js";
|
|
|
14
24
|
|
|
15
25
|
// LLM
|
|
16
26
|
export { ProviderManager } from "./llm/provider.js";
|
|
17
|
-
export type { CompletionRequest, CompletionResponse } from "./llm/provider.js";
|
|
18
27
|
|
|
19
28
|
// Tools
|
|
20
29
|
export { ToolRegistry } from "./tools/registry.js";
|
|
21
30
|
export type { Tool } from "./tools/registry.js";
|
|
31
|
+
export { createDefaultToolset } from "./tools/index.js";
|
|
22
32
|
|
|
23
33
|
// Governance
|
|
24
34
|
export { BoundaryManager } from "./governance/boundary.js";
|
|
25
35
|
export { ReviewGate } from "./governance/review-gate.js";
|
|
36
|
+
|
|
37
|
+
// Config
|
|
38
|
+
export { loadAgentConfig } from "./config.js";
|
package/src/io/context.ts
CHANGED
|
@@ -1,40 +1,84 @@
|
|
|
1
1
|
import type { MemoryStore, MemoryEvent } from "./memory.js";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* Sliding window context manager with compaction.
|
|
5
|
-
*
|
|
6
|
-
* When token usage approaches the configured window, the oldest 50%
|
|
7
|
-
* of events are summarised into a dense state blob to preserve headroom
|
|
8
|
-
* without losing history.
|
|
4
|
+
* Sliding window context manager with token-based compaction.
|
|
9
5
|
*/
|
|
10
6
|
export class ContextManager {
|
|
7
|
+
private encoder?: (text: string) => Promise<number[]> | number[];
|
|
8
|
+
|
|
11
9
|
constructor(
|
|
12
10
|
private readonly memory: MemoryStore,
|
|
13
11
|
private readonly windowTokens: number
|
|
14
|
-
) {
|
|
12
|
+
) {
|
|
13
|
+
this.encoder = undefined;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
private async getEncoder(): Promise<(text: string) => Promise<number[]> | number[]> {
|
|
17
|
+
if (this.encoder) return this.encoder;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
const mod = await import("js-tiktoken");
|
|
21
|
+
const fn =
|
|
22
|
+
(mod as any).encodingForModel?.("gpt-4o") ||
|
|
23
|
+
(mod as any).encoding_for_model?.("gpt-4o") ||
|
|
24
|
+
(mod as any).getEncoding?.("cl100k_base");
|
|
25
|
+
|
|
26
|
+
if (fn) {
|
|
27
|
+
this.encoder = (text: string) => {
|
|
28
|
+
const tokens = fn.encode(text);
|
|
29
|
+
if (Array.isArray(tokens)) return tokens;
|
|
30
|
+
return [];
|
|
31
|
+
};
|
|
32
|
+
return this.encoder;
|
|
33
|
+
}
|
|
34
|
+
} catch {}
|
|
35
|
+
|
|
36
|
+
// Fallback: approximate tokenization (4 chars ~= 1 token)
|
|
37
|
+
this.encoder = (text: string) => {
|
|
38
|
+
const count = Math.max(1, Math.ceil(text.length / 4));
|
|
39
|
+
return new Array(count);
|
|
40
|
+
};
|
|
41
|
+
return this.encoder;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private async countTokens(text: string): Promise<number> {
|
|
45
|
+
const encode = await this.getEncoder();
|
|
46
|
+
const tokens = await encode(text);
|
|
47
|
+
return Array.isArray(tokens) ? tokens.length : 0;
|
|
48
|
+
}
|
|
15
49
|
|
|
16
|
-
/** Return recent events that fit within
|
|
17
|
-
getWindow(): MemoryEvent[] {
|
|
50
|
+
/** Return recent events that fit within token window. */
|
|
51
|
+
async getWindow(): Promise<MemoryEvent[]> {
|
|
18
52
|
const all = this.memory.readAll();
|
|
19
53
|
if (all.length === 0) return [];
|
|
20
54
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let budget_remaining = budget;
|
|
55
|
+
const budget = this.windowTokens;
|
|
56
|
+
let budgetRemaining = budget;
|
|
24
57
|
const window: MemoryEvent[] = [];
|
|
25
58
|
|
|
26
59
|
for (let i = all.length - 1; i >= 0; i--) {
|
|
27
60
|
const serialized = JSON.stringify(all[i]);
|
|
28
|
-
|
|
61
|
+
const tokenCount = await this.countTokens(serialized);
|
|
62
|
+
if (tokenCount > budgetRemaining) break;
|
|
29
63
|
window.unshift(all[i]!);
|
|
30
|
-
|
|
64
|
+
budgetRemaining -= tokenCount;
|
|
31
65
|
}
|
|
32
66
|
|
|
33
67
|
return window;
|
|
34
68
|
}
|
|
35
69
|
|
|
36
70
|
/** Returns true if compaction is needed (>80% of window used). */
|
|
37
|
-
needsCompaction(): boolean {
|
|
38
|
-
|
|
71
|
+
async needsCompaction(): Promise<boolean> {
|
|
72
|
+
const total = await this.estimateTokenCount();
|
|
73
|
+
return total > this.windowTokens * 0.8;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async estimateTokenCount(): Promise<number> {
|
|
77
|
+
const events = this.memory.readAll();
|
|
78
|
+
let total = 0;
|
|
79
|
+
for (const event of events) {
|
|
80
|
+
total += await this.countTokens(JSON.stringify(event));
|
|
81
|
+
}
|
|
82
|
+
return total;
|
|
39
83
|
}
|
|
40
84
|
}
|
package/src/io/memory.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface MemoryEvent {
|
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* Append-only JSONL memory store. Each event is a line of JSON.
|
|
12
|
-
* Provides full audit trail of agent actions and LLM interactions.
|
|
12
|
+
* Provides a full audit trail of agent actions and LLM interactions.
|
|
13
13
|
*/
|
|
14
14
|
export class MemoryStore {
|
|
15
15
|
constructor(public readonly memoryPath: string) {
|
|
@@ -17,27 +17,28 @@ export class MemoryStore {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
append(event: MemoryEvent): void {
|
|
20
|
-
|
|
20
|
+
const payload = this.sanitize(event);
|
|
21
|
+
appendFileSync(this.memoryPath, JSON.stringify(payload) + "\n", "utf-8");
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
readAll(maxBytes = 1024 * 1024): MemoryEvent[] {
|
|
24
25
|
if (!existsSync(this.memoryPath)) return [];
|
|
25
|
-
|
|
26
|
+
|
|
26
27
|
const stat = statSync(this.memoryPath);
|
|
27
28
|
if (stat.size === 0) return [];
|
|
28
|
-
|
|
29
|
+
|
|
29
30
|
const size = Math.min(stat.size, maxBytes);
|
|
30
31
|
const pos = Math.max(0, stat.size - size);
|
|
31
|
-
|
|
32
|
+
|
|
32
33
|
const fd = openSync(this.memoryPath, "r");
|
|
33
34
|
const buffer = Buffer.alloc(size);
|
|
34
35
|
readSync(fd, buffer, 0, size, pos);
|
|
35
36
|
closeSync(fd);
|
|
36
|
-
|
|
37
|
+
|
|
37
38
|
const content = buffer.toString("utf-8");
|
|
38
39
|
const lines = content.split("\n");
|
|
39
|
-
if (pos > 0) lines.shift();
|
|
40
|
-
|
|
40
|
+
if (pos > 0) lines.shift();
|
|
41
|
+
|
|
41
42
|
return lines
|
|
42
43
|
.filter(Boolean)
|
|
43
44
|
.map((line) => {
|
|
@@ -50,7 +51,50 @@ export class MemoryStore {
|
|
|
50
51
|
.filter(Boolean) as MemoryEvent[];
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
/**
|
|
54
|
+
/**
|
|
55
|
+
* Scrub sensitive payloads so API keys never get persisted in memory.
|
|
56
|
+
*/
|
|
57
|
+
private sanitize(event: MemoryEvent): MemoryEvent {
|
|
58
|
+
const keys = this.sensitiveValues();
|
|
59
|
+
const jsonString = JSON.stringify(event);
|
|
60
|
+
let scrubbed = jsonString;
|
|
61
|
+
|
|
62
|
+
for (const secret of keys) {
|
|
63
|
+
if (!secret) continue;
|
|
64
|
+
if (scrubbed.includes(secret)) {
|
|
65
|
+
const token = this.mask(secret);
|
|
66
|
+
scrubbed = scrubbed.split(secret).join(token);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(scrubbed);
|
|
72
|
+
return parsed as MemoryEvent;
|
|
73
|
+
} catch {
|
|
74
|
+
return event;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
private sensitiveValues(): string[] {
|
|
79
|
+
const interesting = [
|
|
80
|
+
"ANTHROPIC_API_KEY",
|
|
81
|
+
"GOOGLE_API_KEY",
|
|
82
|
+
"OPENAI_API_KEY",
|
|
83
|
+
"OLLAMA_HOST",
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const values = interesting
|
|
87
|
+
.map((name) => process.env[name])
|
|
88
|
+
.filter((value): value is string => typeof value === "string" && value.length > 0);
|
|
89
|
+
|
|
90
|
+
return Array.from(new Set(values));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private mask(value: string): string {
|
|
94
|
+
return `${value.slice(0, 3)}...${value.slice(-3)}[redacted]`;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Approximate token count kept for compat; use ContextManager for precise counts. */
|
|
54
98
|
estimatedTokenCount(): number {
|
|
55
99
|
if (!existsSync(this.memoryPath)) return 0;
|
|
56
100
|
const stat = statSync(this.memoryPath);
|