@agentprojectcontext/apx 1.15.3 → 1.15.5
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/package.json +1 -1
- package/src/cli/commands/identity.js +8 -6
- package/src/cli/http.js +16 -2
- package/src/core/config.js +1 -0
- package/src/daemon/api.js +12 -1
- package/src/daemon/index.js +10 -0
- package/src/daemon/super-agent-tools/tools/set-identity.js +3 -3
- package/src/daemon/super-agent.js +16 -0
- package/src/daemon/tools/fetch.js +17 -0
package/package.json
CHANGED
|
@@ -65,12 +65,14 @@ export async function cmdIdentity(args) {
|
|
|
65
65
|
console.log("No identity configured. Run: apx identity wizard");
|
|
66
66
|
return;
|
|
67
67
|
}
|
|
68
|
+
const { readConfig } = await import("../../core/config.js");
|
|
69
|
+
const cfg = readConfig();
|
|
68
70
|
console.log("");
|
|
69
71
|
console.log(` Agent name : ${id.agent_name}`);
|
|
70
72
|
console.log(` Personality : ${id.personality || "(not set)"}`);
|
|
71
73
|
console.log(` Owner : ${id.owner_name}`);
|
|
72
74
|
console.log(` Context : ${id.owner_context || "(not set)"}`);
|
|
73
|
-
console.log(` Language : ${
|
|
75
|
+
console.log(` Language : ${cfg.user?.language || "en"} (set via: apx config set user.language <code>)`);
|
|
74
76
|
console.log(` Last wakeup : ${id.last_wakeup || "(never)"}`);
|
|
75
77
|
console.log(` File : ~/.apx/identity.json`);
|
|
76
78
|
console.log("");
|
|
@@ -113,14 +115,14 @@ export async function runWizard() {
|
|
|
113
115
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
114
116
|
|
|
115
117
|
console.log("\n APX Identity Setup\n");
|
|
116
|
-
console.log("
|
|
117
|
-
console.log("
|
|
118
|
+
console.log(" Defines who the agent is, who it works for, and what extra context");
|
|
119
|
+
console.log(" it carries in every conversation (injected into the system prompt).");
|
|
120
|
+
console.log(" Language is configured separately via: apx config set user.language <code>\n");
|
|
118
121
|
|
|
119
122
|
const agent_name = await ask(rl, " Agent name", existing.agent_name || "APX");
|
|
120
123
|
const personality = await ask(rl, " Personality (comma-separated traits)", existing.personality || "direct, curious, helpful");
|
|
121
124
|
const owner_name = await ask(rl, " Your name", existing.owner_name || "");
|
|
122
|
-
const owner_context = await ask(rl, "
|
|
123
|
-
const language = await ask(rl, " Language for agent messages (e.g. Spanish, English)", existing.language || "English");
|
|
125
|
+
const owner_context = await ask(rl, " Context for the agent (what you build / work on — added to every system prompt)", existing.owner_context || "");
|
|
124
126
|
|
|
125
127
|
console.log("\n Claude Code permissions");
|
|
126
128
|
console.log(" APX can configure Claude Code to allow terminal commands without prompts.");
|
|
@@ -130,7 +132,7 @@ export async function runWizard() {
|
|
|
130
132
|
|
|
131
133
|
rl.close();
|
|
132
134
|
|
|
133
|
-
const id = writeIdentity({ agent_name, personality, owner_name, owner_context,
|
|
135
|
+
const id = writeIdentity({ agent_name, personality, owner_name, owner_context, last_wakeup: null });
|
|
134
136
|
console.log(`\n Identity saved to ~/.apx/identity.json`);
|
|
135
137
|
|
|
136
138
|
if (setupPerms) {
|
package/src/cli/http.js
CHANGED
|
@@ -11,6 +11,12 @@ const __dirname = path.dirname(__filename);
|
|
|
11
11
|
const DEFAULT_PORT = parseInt(process.env.APX_PORT || "7430", 10);
|
|
12
12
|
const DEFAULT_HOST = process.env.APX_HOST || "127.0.0.1";
|
|
13
13
|
|
|
14
|
+
const TOKEN_PATH = path.join(os.homedir(), ".apx", "daemon.token");
|
|
15
|
+
|
|
16
|
+
function readToken() {
|
|
17
|
+
try { return fs.readFileSync(TOKEN_PATH, "utf8").trim(); } catch { return ""; }
|
|
18
|
+
}
|
|
19
|
+
|
|
14
20
|
function baseUrl() {
|
|
15
21
|
return `http://${DEFAULT_HOST}:${DEFAULT_PORT}`;
|
|
16
22
|
}
|
|
@@ -71,9 +77,13 @@ async function request(method, path, body, opts = {}) {
|
|
|
71
77
|
else if (!(await ping())) {
|
|
72
78
|
throw new Error(`apx daemon not running (no response on ${baseUrl()})`);
|
|
73
79
|
}
|
|
80
|
+
const token = readToken();
|
|
74
81
|
const res = await fetch(`${baseUrl()}${path}`, {
|
|
75
82
|
method,
|
|
76
|
-
headers:
|
|
83
|
+
headers: {
|
|
84
|
+
...(body ? { "content-type": "application/json" } : {}),
|
|
85
|
+
...(token ? { "authorization": `Bearer ${token}` } : {}),
|
|
86
|
+
},
|
|
77
87
|
body: body ? JSON.stringify(body) : undefined,
|
|
78
88
|
signal: opts.signal,
|
|
79
89
|
});
|
|
@@ -98,9 +108,13 @@ async function streamRequest(method, path, body, onEvent, opts = {}) {
|
|
|
98
108
|
throw new Error(`apx daemon not running (no response on ${baseUrl()})`);
|
|
99
109
|
}
|
|
100
110
|
|
|
111
|
+
const token = readToken();
|
|
101
112
|
const res = await fetch(`${baseUrl()}${path}`, {
|
|
102
113
|
method,
|
|
103
|
-
headers:
|
|
114
|
+
headers: {
|
|
115
|
+
...(body ? { "content-type": "application/json" } : {}),
|
|
116
|
+
...(token ? { "authorization": `Bearer ${token}` } : {}),
|
|
117
|
+
},
|
|
104
118
|
body: body ? JSON.stringify(body) : undefined,
|
|
105
119
|
signal: opts.signal,
|
|
106
120
|
});
|
package/src/core/config.js
CHANGED
|
@@ -8,6 +8,7 @@ export const CONFIG_PATH = path.join(APX_HOME, "config.json");
|
|
|
8
8
|
export const PID_PATH = path.join(APX_HOME, "daemon.pid");
|
|
9
9
|
export const LOG_PATH = path.join(APX_HOME, "daemon.log");
|
|
10
10
|
export const TELEGRAM_STATE_PATH = path.join(APX_HOME, "telegram-state.json");
|
|
11
|
+
export const TOKEN_PATH = path.join(APX_HOME, "daemon.token");
|
|
11
12
|
// Global channel messages (telegram, direct, whatsapp, …) live here,
|
|
12
13
|
// separated from any project. Structure: ~/.apx/messages/<channel>/YYYY-MM-DD.jsonl
|
|
13
14
|
export const GLOBAL_MESSAGES_DIR = path.join(APX_HOME, "messages");
|
package/src/daemon/api.js
CHANGED
|
@@ -78,7 +78,7 @@ function appendSuperAgentErrorTrace(req, error, details = {}) {
|
|
|
78
78
|
});
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
export function buildApi({ projects, registries, plugins, scheduler, version, startedAt, addProjectGlobally, config }) {
|
|
81
|
+
export function buildApi({ projects, registries, plugins, scheduler, version, startedAt, addProjectGlobally, config, token }) {
|
|
82
82
|
const telegram = plugins?.get("telegram");
|
|
83
83
|
|
|
84
84
|
const app = express();
|
|
@@ -89,6 +89,17 @@ export function buildApi({ projects, registries, plugins, scheduler, version, st
|
|
|
89
89
|
next();
|
|
90
90
|
});
|
|
91
91
|
|
|
92
|
+
// Token auth — skip only /health so the CLI can ping before reading the token.
|
|
93
|
+
if (token) {
|
|
94
|
+
app.use((req, res, next) => {
|
|
95
|
+
if (req.path === "/health") return next();
|
|
96
|
+
const auth = req.get("authorization") || "";
|
|
97
|
+
const provided = auth.startsWith("Bearer ") ? auth.slice(7) : "";
|
|
98
|
+
if (provided !== token) return res.status(401).json({ error: "unauthorized" });
|
|
99
|
+
next();
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
92
103
|
// ---- Tool routers (fetch / browser / search / glob / grep / registry) ----
|
|
93
104
|
// fetch = native HTTP, no Chromium → fast, cheap, default for REST/HTML
|
|
94
105
|
// browser = Puppeteer-backed → heavy, lazy-launched, for JS-rendered pages
|
package/src/daemon/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
6
7
|
import {
|
|
7
8
|
readConfig,
|
|
8
9
|
writeConfig,
|
|
@@ -12,6 +13,7 @@ import {
|
|
|
12
13
|
PID_PATH,
|
|
13
14
|
LOG_PATH,
|
|
14
15
|
APX_HOME,
|
|
16
|
+
TOKEN_PATH,
|
|
15
17
|
} from "../core/config.js";
|
|
16
18
|
import { ProjectManager } from "./db.js";
|
|
17
19
|
import { McpRegistry } from "./mcp-runner.js";
|
|
@@ -76,6 +78,12 @@ function clearPid() {
|
|
|
76
78
|
} catch {}
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
function generateToken() {
|
|
82
|
+
const token = randomBytes(32).toString("hex");
|
|
83
|
+
fs.writeFileSync(TOKEN_PATH, token, { mode: 0o600 });
|
|
84
|
+
return token;
|
|
85
|
+
}
|
|
86
|
+
|
|
79
87
|
class RegistryCache {
|
|
80
88
|
constructor() {
|
|
81
89
|
this.byProjectId = new Map();
|
|
@@ -98,6 +106,7 @@ class RegistryCache {
|
|
|
98
106
|
async function main() {
|
|
99
107
|
ensureHome();
|
|
100
108
|
claimSingleton();
|
|
109
|
+
const token = generateToken();
|
|
101
110
|
|
|
102
111
|
const cfg = readConfig();
|
|
103
112
|
const host = effectiveHost(cfg);
|
|
@@ -138,6 +147,7 @@ async function main() {
|
|
|
138
147
|
plugins,
|
|
139
148
|
scheduler,
|
|
140
149
|
config: cfg,
|
|
150
|
+
token,
|
|
141
151
|
version: PKG.version,
|
|
142
152
|
startedAt,
|
|
143
153
|
addProjectGlobally: (absPath) => {
|
|
@@ -13,20 +13,20 @@ export default {
|
|
|
13
13
|
properties: {
|
|
14
14
|
agent_name: { type: "string", description: "new agent name" },
|
|
15
15
|
owner_name: { type: "string", description: "owner name" },
|
|
16
|
+
owner_context: { type: "string", description: "context injected into every system prompt — what the owner builds / works on" },
|
|
16
17
|
personality: { type: "string", description: "comma-separated personality traits" },
|
|
17
|
-
language: { type: "string", description: "preferred language" },
|
|
18
18
|
confirmed: confirmedProperty("true only after explicit user confirmation for this exact identity update"),
|
|
19
19
|
},
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
},
|
|
23
|
-
makeHandler: ({ requirePermission }) => ({ agent_name, owner_name,
|
|
23
|
+
makeHandler: ({ requirePermission }) => ({ agent_name, owner_name, owner_context, personality, confirmed = false } = {}) => {
|
|
24
24
|
requirePermission("set_identity", { dangerous: true, confirmed });
|
|
25
25
|
const fields = {};
|
|
26
26
|
if (agent_name) fields.agent_name = agent_name;
|
|
27
27
|
if (owner_name) fields.owner_name = owner_name;
|
|
28
|
+
if (owner_context !== undefined) fields.owner_context = owner_context;
|
|
28
29
|
if (personality) fields.personality = personality;
|
|
29
|
-
if (language) fields.language = language;
|
|
30
30
|
if (Object.keys(fields).length === 0) {
|
|
31
31
|
return { ok: true, identity: readIdentity() };
|
|
32
32
|
}
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
extractPseudoToolCalls,
|
|
19
19
|
cleanTextOfPseudoToolCalls,
|
|
20
20
|
} from "./tool-call-parser.js";
|
|
21
|
+
import { readIdentity } from "../core/identity.js";
|
|
21
22
|
|
|
22
23
|
const MAX_TOOL_ITERS = 6;
|
|
23
24
|
|
|
@@ -201,8 +202,23 @@ export async function runSuperAgent({
|
|
|
201
202
|
].join("\n");
|
|
202
203
|
})();
|
|
203
204
|
|
|
205
|
+
// Identity: who the agent is, who it works for, and what extra context the owner provided.
|
|
206
|
+
// Language comes from config.user.language (ISO 639-1) so it stays in sync with transcription.
|
|
207
|
+
const identity = (() => { try { return readIdentity(); } catch { return null; } })();
|
|
208
|
+
const userLang = globalConfig?.user?.language || "en";
|
|
209
|
+
const identityBlock = (() => {
|
|
210
|
+
const lines = ["# Identity"];
|
|
211
|
+
if (identity?.agent_name) lines.push(`Your name is ${identity.agent_name}.`);
|
|
212
|
+
if (identity?.personality) lines.push(`Your personality: ${identity.personality}.`);
|
|
213
|
+
if (identity?.owner_name) lines.push(`Your owner is ${identity.owner_name}.`);
|
|
214
|
+
if (identity?.owner_context) lines.push(`Owner context: ${identity.owner_context}`);
|
|
215
|
+
lines.push(`Always reply in the language with ISO code "${userLang}" unless the user explicitly switches.`);
|
|
216
|
+
return lines.join("\n");
|
|
217
|
+
})();
|
|
218
|
+
|
|
204
219
|
const system = [
|
|
205
220
|
sa.system || DEFAULT_SYSTEM,
|
|
221
|
+
identityBlock,
|
|
206
222
|
permissionNote,
|
|
207
223
|
contextNote,
|
|
208
224
|
"# Registered projects (just the index — call tools for details)",
|
|
@@ -33,6 +33,22 @@ async function getFetch() {
|
|
|
33
33
|
const DEFAULT_TIMEOUT = 30000;
|
|
34
34
|
const MAX_BODY_BYTES = 5 * 1024 * 1024; // 5MB
|
|
35
35
|
|
|
36
|
+
// Block private/link-local ranges and cloud metadata endpoints to prevent SSRF.
|
|
37
|
+
const BLOCKED_HOST_RE = /^(localhost|metadata\.google\.internal\.?)$/i;
|
|
38
|
+
const PRIVATE_IP_RE = /^(127\.\d+\.\d+\.\d+|0\.0\.0\.0|::1|10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|169\.254\.\d+\.\d+|fd[0-9a-f]{2}:)/i;
|
|
39
|
+
|
|
40
|
+
function validateUrl(rawUrl) {
|
|
41
|
+
let parsed;
|
|
42
|
+
try { parsed = new URL(rawUrl); } catch { throw new Error("Invalid URL"); }
|
|
43
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
44
|
+
throw new Error(`Protocol "${parsed.protocol}" is not allowed; use http or https`);
|
|
45
|
+
}
|
|
46
|
+
const host = parsed.hostname.toLowerCase().replace(/^\[|\]$/g, "");
|
|
47
|
+
if (BLOCKED_HOST_RE.test(host) || PRIVATE_IP_RE.test(host)) {
|
|
48
|
+
throw new Error(`Requests to private or link-local addresses are blocked`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
36
52
|
async function readBody(response, jsonHint) {
|
|
37
53
|
const ctype = response.headers.get("content-type") || "";
|
|
38
54
|
const wantsJson = jsonHint || ctype.includes("application/json");
|
|
@@ -57,6 +73,7 @@ async function readBody(response, jsonHint) {
|
|
|
57
73
|
|
|
58
74
|
async function doRequest({ url, method = "GET", headers = {}, body = null, timeout_ms = DEFAULT_TIMEOUT, json = false } = {}) {
|
|
59
75
|
if (!url) throw new Error("url required");
|
|
76
|
+
validateUrl(url);
|
|
60
77
|
const fetch = await getFetch();
|
|
61
78
|
|
|
62
79
|
const controller = new AbortController();
|