xcoder-jarvis-agent 0.0.0-stage → 1.4.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 +21 -0
- package/README.md +243 -2
- package/dist/index.d.ts +27 -0
- package/dist/index.js +28 -0
- package/dist/jarvis/agent.d.ts +50 -0
- package/dist/jarvis/agent.js +305 -0
- package/dist/jarvis/argv.d.ts +30 -0
- package/dist/jarvis/argv.js +138 -0
- package/dist/jarvis/budget.d.ts +49 -0
- package/dist/jarvis/budget.js +83 -0
- package/dist/jarvis/chat/confirm.d.ts +23 -0
- package/dist/jarvis/chat/confirm.js +37 -0
- package/dist/jarvis/chat/lines.d.ts +41 -0
- package/dist/jarvis/chat/lines.js +93 -0
- package/dist/jarvis/chat/prompt.d.ts +10 -0
- package/dist/jarvis/chat/prompt.js +18 -0
- package/dist/jarvis/chat/session.d.ts +46 -0
- package/dist/jarvis/chat/session.js +121 -0
- package/dist/jarvis/chat/terminator.d.ts +14 -0
- package/dist/jarvis/chat/terminator.js +19 -0
- package/dist/jarvis/decision.d.ts +64 -0
- package/dist/jarvis/decision.js +116 -0
- package/dist/jarvis/exit.d.ts +17 -0
- package/dist/jarvis/exit.js +17 -0
- package/dist/jarvis/memory/store.d.ts +88 -0
- package/dist/jarvis/memory/store.js +189 -0
- package/dist/jarvis/prompt/assemble.d.ts +61 -0
- package/dist/jarvis/prompt/assemble.js +118 -0
- package/dist/jarvis/prompt/policy.d.ts +22 -0
- package/dist/jarvis/prompt/policy.js +80 -0
- package/dist/jarvis/prompt/skills.d.ts +69 -0
- package/dist/jarvis/prompt/skills.js +257 -0
- package/dist/jarvis/react-loop.d.ts +51 -0
- package/dist/jarvis/react-loop.js +160 -0
- package/dist/jarvis/telemetry/provenance.d.ts +54 -0
- package/dist/jarvis/telemetry/provenance.js +56 -0
- package/dist/jarvis/test-helpers.d.ts +14 -0
- package/dist/jarvis/test-helpers.js +69 -0
- package/dist/jarvis/tools/fs-tools.d.ts +28 -0
- package/dist/jarvis/tools/fs-tools.js +292 -0
- package/dist/jarvis/tools/memory-tools.d.ts +27 -0
- package/dist/jarvis/tools/memory-tools.js +83 -0
- package/dist/jarvis/tools/registry.d.ts +54 -0
- package/dist/jarvis/tools/registry.js +49 -0
- package/dist/jarvis/util/confirm.d.ts +28 -0
- package/dist/jarvis/util/confirm.js +54 -0
- package/dist/jarvis/util/kill.d.ts +10 -0
- package/dist/jarvis/util/kill.js +39 -0
- package/dist/jarvis/util/text.d.ts +74 -0
- package/dist/jarvis/util/text.js +190 -0
- package/dist/program.d.ts +2 -0
- package/dist/program.js +12 -0
- package/package.json +35 -3
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text and filesystem primitives shared by the prompt assembler, the tools and
|
|
3
|
+
* the memory store. Kept together so the injection and redaction behaviour is
|
|
4
|
+
* auditable in exactly one file (PRM-02, PRM-06, PRM-10).
|
|
5
|
+
*/
|
|
6
|
+
export declare function sha256(input: string | Buffer): string;
|
|
7
|
+
export declare function shortHash(input: string, len?: number): string;
|
|
8
|
+
/**
|
|
9
|
+
* Normalise untrusted text before it enters a prompt (PRM-06).
|
|
10
|
+
*
|
|
11
|
+
* Strips zero-width/bidi controls, collapses CRLF to LF, and normalises to
|
|
12
|
+
* NFC. Deliberately does NOT rewrite ordinary text: the output still reads as
|
|
13
|
+
* the author wrote it, it just cannot carry invisible instructions.
|
|
14
|
+
*/
|
|
15
|
+
export declare function normalizeUntrusted(text: string, maxChars?: number): string;
|
|
16
|
+
/**
|
|
17
|
+
* Escape a closing delimiter so untrusted text cannot break out of its block.
|
|
18
|
+
*
|
|
19
|
+
* An attacker who gets `</untrusted-skill>` into a skill pack could otherwise
|
|
20
|
+
* end the data block early and have the remainder read as instructions. We
|
|
21
|
+
* escape the angle brackets of any delimiter-shaped sequence rather than
|
|
22
|
+
* stripping it, so the text stays visible and reviewable.
|
|
23
|
+
*/
|
|
24
|
+
export declare function escapeDelimiter(text: string, tag: string): string;
|
|
25
|
+
/** Escape every angle bracket/quoter — used for attribute values (name, sha). */
|
|
26
|
+
export declare function escapeAttr(value: string): string;
|
|
27
|
+
export interface Redacted {
|
|
28
|
+
text: string;
|
|
29
|
+
hits: number;
|
|
30
|
+
}
|
|
31
|
+
/** Redact secrets and report how many rules fired (CORE-07). */
|
|
32
|
+
export declare function redact(text: string): Redacted;
|
|
33
|
+
export declare function truncate(text: string, max: number): {
|
|
34
|
+
text: string;
|
|
35
|
+
truncated: boolean;
|
|
36
|
+
};
|
|
37
|
+
export declare function byteLength(text: string): number;
|
|
38
|
+
/**
|
|
39
|
+
* Short, filesystem-safe label derived from a user-supplied scope id.
|
|
40
|
+
*
|
|
41
|
+
* `--user` is an untrusted scope label, and it becomes a filename, so it must
|
|
42
|
+
* not be able to name a directory that resolves above the memory root. All
|
|
43
|
+
* separators become '-', runs of dots collapse to a single '.', and leading
|
|
44
|
+
* dots are stripped — that removes '.' and '..' as possible results, which
|
|
45
|
+
* are the only relative names that traverse.
|
|
46
|
+
*/
|
|
47
|
+
export declare function sanitizeLabel(raw: string, fallback?: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Resolve `candidate` inside `root`, refusing paths that escape it.
|
|
50
|
+
*
|
|
51
|
+
* Uses `realpath` on both sides so a symlink pointing outside the workspace
|
|
52
|
+
* is rejected rather than followed (AI-04 path confinement).
|
|
53
|
+
*/
|
|
54
|
+
export declare function resolveInsideRoot(root: string, candidate: string): Promise<{
|
|
55
|
+
ok: true;
|
|
56
|
+
path: string;
|
|
57
|
+
} | {
|
|
58
|
+
ok: false;
|
|
59
|
+
reason: string;
|
|
60
|
+
}>;
|
|
61
|
+
export declare function ensureDir(dir: string, mode?: number): Promise<void>;
|
|
62
|
+
export declare function writeFilePrivate(file: string, data: string): Promise<void>;
|
|
63
|
+
/** Write via temp file + rename so a reader never sees a half-written file. */
|
|
64
|
+
export declare function writeAtomic(file: string, data: string): Promise<void>;
|
|
65
|
+
/** Read a file, refusing anything larger than `maxBytes` before allocating. */
|
|
66
|
+
export declare function readBounded(file: string, maxBytes: number): Promise<{
|
|
67
|
+
ok: true;
|
|
68
|
+
text: string;
|
|
69
|
+
bytes: number;
|
|
70
|
+
truncated: boolean;
|
|
71
|
+
} | {
|
|
72
|
+
ok: false;
|
|
73
|
+
reason: string;
|
|
74
|
+
}>;
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// xcoder:version 2 | xcoder:task task-f1b5f6 | xcoder:updated 2026-11-05T02:49:12.254Z | xcoder:subtask code-st-53c7b5
|
|
2
|
+
import crypto from "node:crypto";
|
|
3
|
+
import fsp from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { redactSecrets } from "xcoder-dna";
|
|
6
|
+
/**
|
|
7
|
+
* Text and filesystem primitives shared by the prompt assembler, the tools and
|
|
8
|
+
* the memory store. Kept together so the injection and redaction behaviour is
|
|
9
|
+
* auditable in exactly one file (PRM-02, PRM-06, PRM-10).
|
|
10
|
+
*/
|
|
11
|
+
export function sha256(input) {
|
|
12
|
+
return crypto.createHash("sha256").update(input).digest("hex");
|
|
13
|
+
}
|
|
14
|
+
export function shortHash(input, len = 16) {
|
|
15
|
+
return sha256(input).slice(0, len);
|
|
16
|
+
}
|
|
17
|
+
/** Zero-width and bidi-control characters used to hide instructions. */
|
|
18
|
+
const ZERO_WIDTH = /[\u200B-\u200F\u202A-\u202E\u2060-\u2064\uFEFF]/g;
|
|
19
|
+
/**
|
|
20
|
+
* Normalise untrusted text before it enters a prompt (PRM-06).
|
|
21
|
+
*
|
|
22
|
+
* Strips zero-width/bidi controls, collapses CRLF to LF, and normalises to
|
|
23
|
+
* NFC. Deliberately does NOT rewrite ordinary text: the output still reads as
|
|
24
|
+
* the author wrote it, it just cannot carry invisible instructions.
|
|
25
|
+
*/
|
|
26
|
+
export function normalizeUntrusted(text, maxChars = 200_000) {
|
|
27
|
+
const clipped = text.length > maxChars ? text.slice(0, maxChars) : text;
|
|
28
|
+
return clipped.replace(ZERO_WIDTH, "").replace(/\r\n?/g, "\n").normalize("NFC");
|
|
29
|
+
}
|
|
30
|
+
const ENTITIES = {
|
|
31
|
+
"<": "<",
|
|
32
|
+
">": ">",
|
|
33
|
+
"&": "&",
|
|
34
|
+
'"': """,
|
|
35
|
+
"'": "'",
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Escape a closing delimiter so untrusted text cannot break out of its block.
|
|
39
|
+
*
|
|
40
|
+
* An attacker who gets `</untrusted-skill>` into a skill pack could otherwise
|
|
41
|
+
* end the data block early and have the remainder read as instructions. We
|
|
42
|
+
* escape the angle brackets of any delimiter-shaped sequence rather than
|
|
43
|
+
* stripping it, so the text stays visible and reviewable.
|
|
44
|
+
*/
|
|
45
|
+
export function escapeDelimiter(text, tag) {
|
|
46
|
+
const pattern = new RegExp(`<\\s*/?\\s*${tag}\\b`, "gi");
|
|
47
|
+
return text.replace(pattern, (m) => m.replace(/[<>]/g, (c) => ENTITIES[c] ?? c));
|
|
48
|
+
}
|
|
49
|
+
/** Escape every angle bracket/quoter — used for attribute values (name, sha). */
|
|
50
|
+
export function escapeAttr(value) {
|
|
51
|
+
return value.replace(/[<>&"']/g, (c) => ENTITIES[c] ?? c);
|
|
52
|
+
}
|
|
53
|
+
/** Redact secrets and report how many rules fired (CORE-07). */
|
|
54
|
+
export function redact(text) {
|
|
55
|
+
try {
|
|
56
|
+
const result = redactSecrets(text);
|
|
57
|
+
const hits = (result.redactions ?? []).reduce((n, r) => n + (r.count ?? 0), 0);
|
|
58
|
+
return { text: result.text, hits };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// redactSecrets is documented never to throw; if that ever changes, the
|
|
62
|
+
// safe action for a logging path is to withhold the text entirely.
|
|
63
|
+
return { text: "[REDACTED:redaction-failed]", hits: 1 };
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export function truncate(text, max) {
|
|
67
|
+
if (text.length <= max)
|
|
68
|
+
return { text, truncated: false };
|
|
69
|
+
return { text: text.slice(0, max), truncated: true };
|
|
70
|
+
}
|
|
71
|
+
export function byteLength(text) {
|
|
72
|
+
return Buffer.byteLength(text, "utf8");
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Short, filesystem-safe label derived from a user-supplied scope id.
|
|
76
|
+
*
|
|
77
|
+
* `--user` is an untrusted scope label, and it becomes a filename, so it must
|
|
78
|
+
* not be able to name a directory that resolves above the memory root. All
|
|
79
|
+
* separators become '-', runs of dots collapse to a single '.', and leading
|
|
80
|
+
* dots are stripped — that removes '.' and '..' as possible results, which
|
|
81
|
+
* are the only relative names that traverse.
|
|
82
|
+
*/
|
|
83
|
+
export function sanitizeLabel(raw, fallback = "unknown") {
|
|
84
|
+
const cleaned = raw
|
|
85
|
+
.normalize("NFKC")
|
|
86
|
+
.replace(/[^A-Za-z0-9._-]+/g, "-")
|
|
87
|
+
.replace(/\.{2,}/g, ".")
|
|
88
|
+
.replace(/^[.-]+|[.-]+$/g, "")
|
|
89
|
+
.slice(0, 64);
|
|
90
|
+
return cleaned.length > 0 ? cleaned : fallback;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Resolve `candidate` inside `root`, refusing paths that escape it.
|
|
94
|
+
*
|
|
95
|
+
* Uses `realpath` on both sides so a symlink pointing outside the workspace
|
|
96
|
+
* is rejected rather than followed (AI-04 path confinement).
|
|
97
|
+
*/
|
|
98
|
+
export async function resolveInsideRoot(root, candidate) {
|
|
99
|
+
let realRoot;
|
|
100
|
+
try {
|
|
101
|
+
realRoot = await fsp.realpath(root);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return { ok: false, reason: "workspace root is not readable" };
|
|
105
|
+
}
|
|
106
|
+
const target = path.resolve(realRoot, candidate);
|
|
107
|
+
let realTarget;
|
|
108
|
+
try {
|
|
109
|
+
realTarget = await fsp.realpath(target);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return { ok: false, reason: "path does not exist" };
|
|
113
|
+
}
|
|
114
|
+
const rel = path.relative(realRoot, realTarget);
|
|
115
|
+
if (rel.startsWith("..") || path.isAbsolute(rel)) {
|
|
116
|
+
return { ok: false, reason: "path escapes the workspace" };
|
|
117
|
+
}
|
|
118
|
+
return { ok: true, path: realTarget };
|
|
119
|
+
}
|
|
120
|
+
export async function ensureDir(dir, mode = 0o700) {
|
|
121
|
+
await fsp.mkdir(dir, { recursive: true, mode });
|
|
122
|
+
// Windows ignores POSIX modes; best-effort is documented in the README.
|
|
123
|
+
if (process.platform !== "win32") {
|
|
124
|
+
try {
|
|
125
|
+
await fsp.chmod(dir, mode);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
/* best effort */
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
export async function writeFilePrivate(file, data) {
|
|
133
|
+
await ensureDir(path.dirname(file));
|
|
134
|
+
await fsp.writeFile(file, data, { encoding: "utf8", mode: 0o600 });
|
|
135
|
+
if (process.platform !== "win32") {
|
|
136
|
+
try {
|
|
137
|
+
await fsp.chmod(file, 0o600);
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
/* best effort */
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
/** Write via temp file + rename so a reader never sees a half-written file. */
|
|
145
|
+
export async function writeAtomic(file, data) {
|
|
146
|
+
await ensureDir(path.dirname(file));
|
|
147
|
+
const tmp = `${file}.tmp-${process.pid}-${crypto.randomBytes(4).toString("hex")}`;
|
|
148
|
+
await fsp.writeFile(tmp, data, { encoding: "utf8", mode: 0o600 });
|
|
149
|
+
await fsp.rename(tmp, file);
|
|
150
|
+
if (process.platform !== "win32") {
|
|
151
|
+
try {
|
|
152
|
+
await fsp.chmod(file, 0o600);
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
/* best effort */
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/** Read a file, refusing anything larger than `maxBytes` before allocating. */
|
|
160
|
+
export async function readBounded(file, maxBytes) {
|
|
161
|
+
let stat;
|
|
162
|
+
try {
|
|
163
|
+
stat = await fsp.stat(file);
|
|
164
|
+
}
|
|
165
|
+
catch {
|
|
166
|
+
return { ok: false, reason: "file does not exist" };
|
|
167
|
+
}
|
|
168
|
+
if (!stat.isFile())
|
|
169
|
+
return { ok: false, reason: "not a regular file" };
|
|
170
|
+
const limit = Math.min(stat.size, maxBytes);
|
|
171
|
+
try {
|
|
172
|
+
const handle = await fsp.open(file, "r");
|
|
173
|
+
try {
|
|
174
|
+
const buf = Buffer.alloc(limit);
|
|
175
|
+
const { bytesRead } = await handle.read(buf, 0, limit, 0);
|
|
176
|
+
return {
|
|
177
|
+
ok: true,
|
|
178
|
+
text: buf.subarray(0, bytesRead).toString("utf8"),
|
|
179
|
+
bytes: bytesRead,
|
|
180
|
+
truncated: stat.size > limit,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
finally {
|
|
184
|
+
await handle.close();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
return { ok: false, reason: "file could not be read" };
|
|
189
|
+
}
|
|
190
|
+
}
|
package/dist/program.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// xcoder:version 1 | xcoder:task task-f1b5f6 | xcoder:updated 2026-11-05T02:47:38.121Z | xcoder:subtask code-st-53c7b5
|
|
3
|
+
import { runJarvisAgent } from "./index.js";
|
|
4
|
+
/**
|
|
5
|
+
* Process boundary only (design §2.1).
|
|
6
|
+
*
|
|
7
|
+
* Everything testable lives behind `runJarvisAgent`; this file exists so the
|
|
8
|
+
* manifest's `bin.jarvis` entry has something to point at, and so the exit
|
|
9
|
+
* code the CLI contract promises is the one the process actually returns.
|
|
10
|
+
*/
|
|
11
|
+
const code = await runJarvisAgent(process.argv.slice(2));
|
|
12
|
+
process.exit(code);
|
package/package.json
CHANGED
|
@@ -1,6 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "xcoder-jarvis-agent",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "1.4.0",
|
|
4
|
+
"description": "Jarvis — a ReAct agent with scoped memory, skill-pack prompt assembly and a code-enforced tool allowlist, built on xcoder-dna.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Neural Node Labs",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"engines": {
|
|
9
|
+
"node": ">=20"
|
|
10
|
+
},
|
|
11
|
+
"publishConfig": {
|
|
12
|
+
"access": "public"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE"
|
|
18
|
+
],
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./package.json": "./package.json"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"jarvis": "./dist/program.js"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsc -p tsconfig.json",
|
|
31
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
32
|
+
"test": "vitest run"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"xcoder-dna": "^1.4.0",
|
|
36
|
+
"zod": "^4.4.3"
|
|
37
|
+
}
|
|
6
38
|
}
|