@agentprojectcontext/apx 1.67.0 → 1.69.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/package.json +1 -1
- package/src/core/config/redact.js +44 -0
- package/src/host/daemon/api/agents.js +37 -1
- package/src/host/daemon/api/config.js +17 -5
- package/src/host/daemon/api/sessions.js +9 -0
- package/src/interfaces/web/dist/assets/index-_2zKBH4O.js +803 -0
- package/src/interfaces/web/dist/assets/index-_2zKBH4O.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-xQYf6_ab.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +3 -3
- package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +46 -31
- package/src/interfaces/web/src/components/config/project-config-sections.ts +9 -11
- package/src/interfaces/web/src/components/memory/MemoryBrowser.tsx +162 -0
- package/src/interfaces/web/src/i18n/en.ts +10 -0
- package/src/interfaces/web/src/i18n/es.ts +10 -0
- package/src/interfaces/web/src/lib/api/agents.ts +2 -1
- package/src/interfaces/web/src/lib/api/sessions.ts +2 -1
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +50 -60
- package/src/interfaces/web/src/screens/base/SessionsTab.tsx +11 -5
- package/src/interfaces/web/src/screens/project/AgentBrainGraph.tsx +169 -47
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +108 -34
- package/src/interfaces/web/src/screens/project/AgentsTab.tsx +72 -16
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +110 -25
- package/src/interfaces/web/src/screens/project/MemoriesTab.tsx +7 -128
- package/src/interfaces/web/src/screens/project/Overview.tsx +93 -3
- package/src/interfaces/web/src/types/daemon.ts +10 -0
- package/src/interfaces/web/dist/assets/index-B3pEwe1m.js +0 -803
- package/src/interfaces/web/dist/assets/index-B3pEwe1m.js.map +0 -1
- package/src/interfaces/web/dist/assets/index-BPGECxzm.css +0 -1
package/package.json
CHANGED
|
@@ -67,6 +67,50 @@ export function redactConfig(cfg) {
|
|
|
67
67
|
return out;
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/** Walk a dotted path on an object, returning the value or undefined. */
|
|
71
|
+
function getDotted(obj, dotted) {
|
|
72
|
+
let cur = obj;
|
|
73
|
+
for (const part of dotted.split(".")) {
|
|
74
|
+
if (!cur || typeof cur !== "object") return undefined;
|
|
75
|
+
cur = cur[part];
|
|
76
|
+
}
|
|
77
|
+
return cur;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Set a dotted path on an object, creating intermediate objects. */
|
|
81
|
+
function setDotted(obj, dotted, value) {
|
|
82
|
+
const parts = dotted.split(".");
|
|
83
|
+
let cur = obj;
|
|
84
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
85
|
+
if (!cur[parts[i]] || typeof cur[parts[i]] !== "object") cur[parts[i]] = {};
|
|
86
|
+
cur = cur[parts[i]];
|
|
87
|
+
}
|
|
88
|
+
cur[parts[parts.length - 1]] = value;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Merge a possibly-redacted config `next` against the prior on-disk config.
|
|
93
|
+
* Anywhere a secret path in `next` holds a marker (the value a redacted view
|
|
94
|
+
* echoes back), restore the prior real secret — so saving the redacted view
|
|
95
|
+
* never clobbers a real key with the literal "*** set ***" string. Mutates and
|
|
96
|
+
* returns `next`.
|
|
97
|
+
*/
|
|
98
|
+
export function mergeRedactedSecrets(next, prior) {
|
|
99
|
+
if (!next || typeof next !== "object") return next;
|
|
100
|
+
for (const dotted of SECRET_PATHS) {
|
|
101
|
+
if (dotted.includes("*")) continue;
|
|
102
|
+
if (isSecretMarker(getDotted(next, dotted))) {
|
|
103
|
+
const priorVal = getDotted(prior, dotted);
|
|
104
|
+
if (typeof priorVal === "string" && priorVal.length) setDotted(next, dotted, priorVal);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const nextChannels = next?.telegram?.channels;
|
|
108
|
+
if (Array.isArray(nextChannels)) {
|
|
109
|
+
next.telegram.channels = mergeRedactedChannels(nextChannels, prior?.telegram?.channels);
|
|
110
|
+
}
|
|
111
|
+
return next;
|
|
112
|
+
}
|
|
113
|
+
|
|
70
114
|
/** Redact a single Telegram channel record. */
|
|
71
115
|
export function redactChannel(channel) {
|
|
72
116
|
if (!channel?.bot_token) return channel;
|
|
@@ -25,6 +25,40 @@ import {
|
|
|
25
25
|
import { agentToResponse } from "./shared.js";
|
|
26
26
|
import { normalizeVaultPatch } from "#core/apc/agents-vault.js";
|
|
27
27
|
import { PERMISSION_MODES } from "#core/constants/permissions.js";
|
|
28
|
+
import { listConversations } from "#core/stores/conversations.js";
|
|
29
|
+
import { listTasks } from "#core/stores/tasks.js";
|
|
30
|
+
import { listRoutines } from "#core/stores/routines.js";
|
|
31
|
+
import { readProjectMessages } from "#core/stores/messages.js";
|
|
32
|
+
|
|
33
|
+
// Attach a per-agent activity summary ({ threads, records, tasks, heartbeats })
|
|
34
|
+
// to a list of agent responses. Reads each store once and tallies by agent, so
|
|
35
|
+
// the whole list costs O(stores) rather than O(agents × stores). Gated behind
|
|
36
|
+
// `?stats=1` on the list endpoint since it touches the message ledger.
|
|
37
|
+
function attachAgentStats(p, agents) {
|
|
38
|
+
const store = p.storagePath || p.path;
|
|
39
|
+
const tally = (rows, key) => {
|
|
40
|
+
const m = Object.create(null);
|
|
41
|
+
for (const r of rows) {
|
|
42
|
+
const a = typeof key === "function" ? key(r) : r?.[key];
|
|
43
|
+
if (a) m[a] = (m[a] || 0) + 1;
|
|
44
|
+
}
|
|
45
|
+
return m;
|
|
46
|
+
};
|
|
47
|
+
let tasksByAgent = {}, hbByAgent = {}, recByAgent = {};
|
|
48
|
+
try { tasksByAgent = tally(listTasks(store, { state: "all" }), "agent"); } catch { /* no task store */ }
|
|
49
|
+
try { hbByAgent = tally(listRoutines(store), (r) => r?.spec?.agent); } catch { /* no routines */ }
|
|
50
|
+
try { recByAgent = tally(readProjectMessages(store, { limit: 1000 }), "agent_slug"); } catch { /* no ledger */ }
|
|
51
|
+
for (const a of agents) {
|
|
52
|
+
let threads = 0;
|
|
53
|
+
try { threads = listConversations(store, a.slug).length; } catch { /* none */ }
|
|
54
|
+
a.stats = {
|
|
55
|
+
threads,
|
|
56
|
+
records: recByAgent[a.slug] || 0,
|
|
57
|
+
tasks: tasksByAgent[a.slug] || 0,
|
|
58
|
+
heartbeats: hbByAgent[a.slug] || 0,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}
|
|
28
62
|
|
|
29
63
|
// Autonomy mirrors the super-agent permission modes (total/automatico/permiso).
|
|
30
64
|
// An invalid value is dropped rather than persisted so a typo can't silently
|
|
@@ -123,7 +157,9 @@ export function register(app, { projects, project }) {
|
|
|
123
157
|
app.get("/projects/:pid/agents", (req, res) => {
|
|
124
158
|
const p = project(req, res);
|
|
125
159
|
if (!p) return;
|
|
126
|
-
|
|
160
|
+
const agents = readAgents(p.path).map(agentToResponse);
|
|
161
|
+
if (req.query.stats === "1") attachAgentStats(p, agents);
|
|
162
|
+
res.json(agents);
|
|
127
163
|
});
|
|
128
164
|
|
|
129
165
|
app.get("/projects/:pid/agents/:slug", (req, res) => {
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
unsetDottedKey,
|
|
12
12
|
} from "../project-config.js";
|
|
13
13
|
import { apcProjectFile, apcProjectConfigFile } from "#core/apc/paths.js";
|
|
14
|
+
import { redactConfig, mergeRedactedSecrets, isSecretMarker } from "#core/config/redact.js";
|
|
14
15
|
|
|
15
16
|
const projectJsonPath = apcProjectFile;
|
|
16
17
|
|
|
@@ -30,9 +31,12 @@ export function register(app, { projects, project }) {
|
|
|
30
31
|
app.get("/projects/:pid/config", (req, res) => {
|
|
31
32
|
const p = project(req, res);
|
|
32
33
|
if (!p) return;
|
|
34
|
+
// Redact secrets (engine api_keys, telegram bot tokens) the same way the
|
|
35
|
+
// global admin config does — the UI shows "…XXXXX" and echoes the marker
|
|
36
|
+
// back unchanged, which the write paths below restore from disk.
|
|
33
37
|
res.json({
|
|
34
|
-
effective: p.config || {},
|
|
35
|
-
project_only: readProjectConfig(p.path),
|
|
38
|
+
effective: redactConfig(p.config || {}),
|
|
39
|
+
project_only: redactConfig(readProjectConfig(p.path)),
|
|
36
40
|
project_config_path: apcProjectConfigFile(p.path),
|
|
37
41
|
apc_project: readProjectJson(p.path),
|
|
38
42
|
project_json_path: projectJsonPath(p.path),
|
|
@@ -45,7 +49,10 @@ export function register(app, { projects, project }) {
|
|
|
45
49
|
const body = req.body || {};
|
|
46
50
|
if (typeof body !== "object" || Array.isArray(body))
|
|
47
51
|
return res.status(400).json({ error: "body must be a JSON object" });
|
|
48
|
-
|
|
52
|
+
// A redacted secret echoed back means "keep the real value" — restore it
|
|
53
|
+
// from disk so a full replace of the redacted view can't wipe secrets.
|
|
54
|
+
const merged = mergeRedactedSecrets(body, readProjectConfig(p.path));
|
|
55
|
+
writeProjectConfig(p.path, merged);
|
|
49
56
|
projects.rebuild(p.id);
|
|
50
57
|
res.json({ ok: true });
|
|
51
58
|
});
|
|
@@ -56,14 +63,19 @@ export function register(app, { projects, project }) {
|
|
|
56
63
|
const { set, unset } = req.body || {};
|
|
57
64
|
const cfg = readProjectConfig(p.path);
|
|
58
65
|
if (set && typeof set === "object") {
|
|
59
|
-
|
|
66
|
+
// Skip echoed secret markers so a redacted field left untouched keeps its
|
|
67
|
+
// real value instead of being overwritten with the marker string.
|
|
68
|
+
for (const [k, v] of Object.entries(set)) {
|
|
69
|
+
if (isSecretMarker(v)) continue;
|
|
70
|
+
setDottedKey(cfg, k, v);
|
|
71
|
+
}
|
|
60
72
|
}
|
|
61
73
|
if (Array.isArray(unset)) {
|
|
62
74
|
for (const k of unset) unsetDottedKey(cfg, k);
|
|
63
75
|
}
|
|
64
76
|
writeProjectConfig(p.path, cfg);
|
|
65
77
|
projects.rebuild(p.id);
|
|
66
|
-
res.json({ ok: true, project_only: cfg });
|
|
78
|
+
res.json({ ok: true, project_only: redactConfig(cfg) });
|
|
67
79
|
});
|
|
68
80
|
|
|
69
81
|
app.put("/projects/:pid/apc-project", (req, res) => {
|
|
@@ -25,12 +25,21 @@ export function register(app, { projects, project }) {
|
|
|
25
25
|
const engineId = req.query.engine ? String(req.query.engine) : null;
|
|
26
26
|
const q = req.query.q ? String(req.query.q) : "";
|
|
27
27
|
const deep = req.query.deep === "1" || req.query.deep === "true";
|
|
28
|
+
// Optional ?cwd= scopes to sessions whose working dir is that folder (or a
|
|
29
|
+
// child of it) — used by the per-project Sessions view. Omitted = all.
|
|
30
|
+
const cwd = req.query.cwd ? String(req.query.cwd).replace(/\/+$/, "") : "";
|
|
28
31
|
let rows = [];
|
|
29
32
|
try {
|
|
30
33
|
rows = collectAllSessions({}, { engineId });
|
|
31
34
|
} catch (e) {
|
|
32
35
|
return res.status(500).json({ error: e.message, meta: { total: 0, offset: 0, limit: null, pageSize: 0, page: 1, pageCount: 1 }, data: [] });
|
|
33
36
|
}
|
|
37
|
+
if (cwd) {
|
|
38
|
+
rows = rows.filter((r) => {
|
|
39
|
+
const c = (r.cwd || "").replace(/\/+$/, "");
|
|
40
|
+
return c === cwd || c.startsWith(cwd + "/");
|
|
41
|
+
});
|
|
42
|
+
}
|
|
34
43
|
if (q.trim()) {
|
|
35
44
|
// filterSessionsByQuery already de-dupes and sorts newest-first.
|
|
36
45
|
rows = filterSessionsByQuery(rows, { query: q, deep });
|