total-recall-brain 3.40.6 → 3.41.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/bin/total-recall-memory.mjs +0 -0
- package/bin/total-recall.mjs +9 -40
- package/docs/reference/cli-reference.md +65 -1
- package/frontend/dist/assets/{index-AGUes-VG.css → index-BG9NkVUy.css} +1 -1
- package/frontend/dist/assets/{index-Bt5iVPZj.js → index-BtuiZqwP.js} +20 -7
- package/frontend/dist/index.html +2 -2
- package/frontend/src/components/plugins/PluginPanel.tsx +31 -5
- package/frontend/src/components/plugins/card/CardEngine.tsx +15 -23
- package/frontend/src/components/plugins/card/CardScenes.tsx +52 -23
- package/frontend/src/components/plugins/card/card.css +2 -5
- package/frontend/src/components/plugins/card/contract.ts +13 -19
- package/frontend/src/components/plugins/card/format.ts +8 -1
- package/frontend/src/components/plugins/card/widgets.tsx +4 -35
- package/frontend/src/components/plugins/plugin-frame.ts +30 -3
- package/frontend/src/pages/PluginsPage.tsx +3 -2
- package/package.json +1 -1
- package/scaffold/.agent/skills/plugin/SKILL.md +110 -0
- package/scaffold/.agent/skills/plugin/evals/README.md +53 -0
- package/scaffold/.agent/skills/plugin/evals/cases/01-fabricated-seed.mjs +42 -0
- package/scaffold/.agent/skills/plugin/evals/cases/02-reassuring-default.mjs +39 -0
- package/scaffold/.agent/skills/plugin/evals/cases/03-declared-template.mjs +32 -0
- package/scaffold/.agent/skills/plugin/evals/cases/04-card-collects-credential.mjs +46 -0
- package/scaffold/.agent/skills/plugin/evals/cases/05-host-leak-in-scaffold.mjs +55 -0
- package/scaffold/.agent/skills/plugin/evals/cases/06-correct-plugin.mjs +92 -0
- package/scaffold/.agent/skills/plugin/evals/run.mjs +118 -0
- package/scaffold/.agent/skills/plugin/prompts/01-function.md +29 -0
- package/scaffold/.agent/skills/plugin/prompts/02-surfaces.md +43 -0
- package/scaffold/.agent/skills/plugin/prompts/03-scaffold.md +31 -0
- package/scaffold/.agent/skills/plugin/prompts/04-cli.md +49 -0
- package/scaffold/.agent/skills/plugin/prompts/05-credentials.md +41 -0
- package/scaffold/.agent/skills/plugin/prompts/06-card.md +46 -0
- package/scaffold/.agent/skills/plugin/prompts/07-scaffold.md +52 -0
- package/scaffold/.agent/skills/plugin/prompts/08-verify.md +43 -0
- package/scaffold/.agent/skills/plugin/references/anti-patterns.md +123 -0
- package/scaffold/.agent/skills/plugin/references/card.md +90 -0
- package/scaffold/.agent/skills/plugin/references/scaffold.md +120 -0
- package/scaffold/.agent/skills/plugin/references/surfaces.md +206 -0
- package/scaffold/.agent/skills/plugin/scripts/record.mjs +360 -0
- package/scaffold/.agent/skills/plugin/subagents/card-designer.md +60 -0
- package/scaffold/.agent/skills/plugin/subagents/plugin-reviewer.md +55 -0
- package/scaffold/.agent/skills/plugin/subagents/scaffold-extractor.md +71 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/package-lock.json +71 -1441
- package/scaffold/.agent/skills/total-recall/plugins/auth/package.json +7 -5
- package/scaffold/.agent/skills/total-recall/plugins/auth/plugin.json +3 -44
- package/scaffold/.agent/skills/total-recall/plugins/auth/scripts/agent-login-pty.py +185 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/cli-secret.mjs +20 -1
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/host-secrets-store.mjs +15 -0
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-env-export.mjs +6 -4
- package/scaffold/.agent/skills/total-recall/plugins/auth/src/secrets-remote-deploy.mjs +1 -0
- package/scaffold/.agent/skills/total-recall/plugins/cli-agents/plugin.json +0 -24
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/code-quality/plugin.json +0 -21
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/composable-cli/plugin.json +0 -32
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/embeddings/plugin.json +11 -0
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/README.md +5 -2
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/plugin.json +1 -28
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/DESIGN.md +95 -13
- package/scaffold/.agent/skills/total-recall/plugins/git-sentinel/ui/card.js +553 -340
- package/scaffold/.agent/skills/total-recall/plugins/mesh/plugin.json +0 -36
- package/scaffold/.agent/skills/total-recall/plugins/mesh/ui/card.js +383 -400
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/package.json +0 -1
- package/scaffold/.agent/skills/total-recall/plugins/openwiki/plugin.json +0 -28
- package/scaffold/.agent/skills/total-recall/plugins/security/package-lock.json +4 -4
- package/scaffold/.agent/skills/total-recall/plugins/security/plugin.json +0 -32
- package/scaffold/.agent/skills/total-recall/references/auth.md +3 -1
- package/scaffold/.agent/skills/total-recall/references/memory.md +4 -0
- package/scaffold/.agent/skills/total-recall/references/openwiki.md +2 -0
- package/scaffold/.agent/skills/total-recall/references/operations.md +12 -0
- package/src/cli/brain.mjs +4 -2
- package/src/cli/context-stats.mjs +104 -0
- package/src/cli/context.mjs +8 -0
- package/src/cli/daemon.mjs +10 -1
- package/src/cli/export.mjs +3 -2
- package/src/cli/ingest-okf.mjs +12 -15
- package/src/cli/init.mjs +45 -85
- package/src/cli/lint.mjs +3 -2
- package/src/cli/plugin/copies.mjs +68 -0
- package/src/cli/plugin/create.mjs +22 -24
- package/src/cli/plugin/index.mjs +49 -3
- package/src/cli/plugin/search.mjs +19 -2
- package/src/cli/rebuild.mjs +2 -0
- package/src/cli/remember.mjs +23 -0
- package/src/cli/status.mjs +10 -3
- package/src/cli/update.mjs +3 -2
- package/src/core/action-predictor.mjs +161 -0
- package/src/core/app-deploy/runtime-cli/node.mjs.tmpl +20 -3
- package/src/core/app-deploy/source.mjs +16 -9
- package/src/core/automatic-brain-updates.mjs +6 -3
- package/src/core/brain-registry.mjs +19 -2
- package/src/core/brain-state.mjs +23 -0
- package/src/core/cli-commands.mjs +1 -0
- package/src/core/cli-maintenance.mjs +1 -1
- package/src/core/cli-semantic-index.mjs +1 -1
- package/src/core/command-surface.mjs +3 -3
- package/src/core/composed-command-resolution.mjs +21 -0
- package/src/core/config.mjs +84 -0
- package/src/core/confined-memory.mjs +93 -0
- package/src/core/context-telemetry.mjs +214 -0
- package/src/core/daemon-control.mjs +14 -43
- package/src/core/fast-recall.mjs +1 -1
- package/src/core/memory-brain.mjs +2 -0
- package/src/core/plugin-activation.mjs +38 -6
- package/src/core/plugin-bundle.mjs +54 -0
- package/src/core/plugin-catalog.mjs +1 -1
- package/src/core/plugin-context.mjs +1 -1
- package/src/core/plugin-contracts/card.mjs +69 -37
- package/src/core/plugin-loader.mjs +5 -6
- package/src/core/plugin-runner.mjs +13 -6
- package/src/core/plugin-runtime.mjs +5 -4
- package/src/core/plugin-setup.mjs +5 -3
- package/src/core/plugin-state-store.mjs +12 -1
- package/src/core/plugin-state.mjs +46 -5
- package/src/core/plugin-store.mjs +78 -3
- package/src/core/plugin-tasks.mjs +3 -1
- package/src/core/plugin-updates.mjs +117 -0
- package/src/core/plugin-vendor.mjs +10 -4
- package/src/core/project-brain.mjs +14 -41
- package/src/core/project-id.mjs +4 -1
- package/src/core/required-plugins.mjs +151 -13
- package/src/core/skills-registry.mjs +27 -3
- package/src/core/ssss-kernel-bridge.mjs +3 -1
- package/src/core/startup-health.mjs +66 -16
- package/src/core/surface.mjs +51 -44
- package/src/core/testing/plugin-fixture.mjs +69 -0
- package/src/core/vault-files.mjs +13 -0
- package/src/core/vault.mjs +4 -30
- package/src/server/index.mjs +13 -1
- package/src/server/invoke-decision-child.mjs +49 -0
- package/src/server/invoke-execution.mjs +87 -0
- package/src/server/invoke-policy.mjs +64 -0
- package/src/server/invoke-read-child.mjs +26 -0
- package/src/server/invoke-targets.mjs +49 -0
- package/src/server/invoke-write-child.mjs +19 -0
- package/src/server/routes/invoke.mjs +314 -0
- package/src/server/routes/plugins.mjs +1 -1
- package/scaffold/.agent/skills/plugins/SKILL.md +0 -57
- package/scaffold/.agent/skills/plugins/references/card-design.md +0 -55
- package/scaffold/.agent/skills/plugins/references/distribution.md +0 -23
- package/scaffold/.agent/skills/plugins/references/manifest-contract.md +0 -90
- package/scaffold/.agent/skills/plugins/references/optimized/index.md +0 -9
- package/scaffold/.agent/skills/plugins/references/optimized/manifest.json +0 -35
- package/scaffold/.agent/skills/plugins/references/optimized/original.md +0 -58
- package/scaffold/.agent/skills/plugins/references/optimized/section-01.md +0 -5
- package/scaffold/.agent/skills/plugins/references/optimized/section-02.md +0 -25
- package/scaffold/.agent/skills/plugins/references/optimized/section-03.md +0 -12
- package/scaffold/.agent/skills/plugins/references/optimized/section-04.md +0 -7
- package/scaffold/.agent/skills/plugins/references/optimized/section-05.md +0 -5
- package/scaffold/.agent/skills/total-recall/openwiki/README.md +0 -10
- package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +0 -5
- package/scaffold/.agent/skills/total-recall/openwiki/ide.md +0 -10
- package/scaffold/.agent/skills/total-recall/openwiki/memory.md +0 -11
- package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +0 -7
- package/scaffold/.agent/skills/total-recall/openwiki/skills.md +0 -7
- package/scaffold/openwiki/README.md +0 -10
- package/scaffold/openwiki/architecture.md +0 -5
- package/scaffold/openwiki/ide.md +0 -10
- package/scaffold/openwiki/memory.md +0 -11
- package/scaffold/openwiki/secrets.md +0 -7
- package/scaffold/openwiki/skills.md +0 -7
- package/src/core/okf-adapter.mjs +0 -559
- package/templates/openwiki/README.md +0 -10
- package/templates/openwiki/architecture.md +0 -5
- package/templates/openwiki/ide.md +0 -10
- package/templates/openwiki/memory.md +0 -11
- package/templates/openwiki/secrets.md +0 -7
- package/templates/openwiki/skills.md +0 -7
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fixture manifests for specs that exercise plumbing rather than card design.
|
|
3
|
+
*
|
|
4
|
+
* Every real plugin has a bespoke card face declared in `ui.elements[]`, and the
|
|
5
|
+
* validator enforces it. Specs that install, copy, vendor, discover or compile a
|
|
6
|
+
* plugin still need a manifest that satisfies that contract, or they fail on a
|
|
7
|
+
* rule they are not testing. This module supplies the smallest honest face so
|
|
8
|
+
* those specs keep asserting what they mean to assert.
|
|
9
|
+
*
|
|
10
|
+
* The face here is deliberately generic and exists only in tests. Production
|
|
11
|
+
* plugin UI is always hand-authored per plugin (invariants-b6e8fcac).
|
|
12
|
+
*/
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
|
|
16
|
+
const nodeFs = fs;
|
|
17
|
+
const nodePath = path;
|
|
18
|
+
|
|
19
|
+
/** The smallest valid card face. Declared once, as every plugin declares one. */
|
|
20
|
+
export const FIXTURE_CARD_FACE = Object.freeze({
|
|
21
|
+
design_tokens: 'ui/DESIGN.md',
|
|
22
|
+
elements: [
|
|
23
|
+
{
|
|
24
|
+
id: 'card',
|
|
25
|
+
kind: 'card',
|
|
26
|
+
tag: 'tr-fixture-card',
|
|
27
|
+
module: 'ui/card.js',
|
|
28
|
+
description: 'Fixture card face',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/** Source of the element module referenced by FIXTURE_CARD_FACE. */
|
|
34
|
+
export const FIXTURE_CARD_SOURCE = 'export const tag = "tr-fixture-card";\n';
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Merge a bespoke face into a fixture manifest.
|
|
38
|
+
*
|
|
39
|
+
* Pass `ui: null` to omit the face deliberately, e.g. when asserting that a
|
|
40
|
+
* plugin without one is rejected.
|
|
41
|
+
*/
|
|
42
|
+
export function withFixtureFace(manifest = {}) {
|
|
43
|
+
if (manifest.ui === null) {
|
|
44
|
+
const { ui: _omitted, ...rest } = manifest;
|
|
45
|
+
return rest;
|
|
46
|
+
}
|
|
47
|
+
return { ...manifest, ui: manifest.ui ?? { ...FIXTURE_CARD_FACE } };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Write `plugin.json` for a fixture plugin, declaring a face unless the manifest
|
|
52
|
+
* explicitly opts out with `ui: null`.
|
|
53
|
+
*
|
|
54
|
+
* Returns the manifest that was written.
|
|
55
|
+
*/
|
|
56
|
+
export function writeFixturePlugin(pluginDir, manifest, { fs = nodeFs, path = nodePath } = {}) {
|
|
57
|
+
const written = withFixtureFace(manifest);
|
|
58
|
+
nodeFs.writeFileSync(
|
|
59
|
+
nodePath.join(pluginDir, 'plugin.json'),
|
|
60
|
+
JSON.stringify(written, null, 2),
|
|
61
|
+
);
|
|
62
|
+
if (written.ui?.elements?.length) {
|
|
63
|
+
const uiDir = nodePath.join(pluginDir, 'ui');
|
|
64
|
+
nodeFs.mkdirSync(uiDir, { recursive: true });
|
|
65
|
+
nodeFs.writeFileSync(nodePath.join(uiDir, 'card.js'), FIXTURE_CARD_SOURCE);
|
|
66
|
+
nodeFs.writeFileSync(nodePath.join(uiDir, 'DESIGN.md'), '# Fixture card\n');
|
|
67
|
+
}
|
|
68
|
+
return written;
|
|
69
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Pure filesystem helpers. Importing these must not load auth or configuration. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
|
|
4
|
+
export function isSafeVaultName(name) {
|
|
5
|
+
const value = String(name);
|
|
6
|
+
return /^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/.test(value) && !value.includes('..');
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function atomicWrite(filePath, content) {
|
|
10
|
+
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
11
|
+
fs.writeFileSync(tmpPath, content, 'utf8');
|
|
12
|
+
fs.renameSync(tmpPath, filePath);
|
|
13
|
+
}
|
package/src/core/vault.mjs
CHANGED
|
@@ -2,37 +2,11 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import matter from './frontmatter.mjs';
|
|
4
4
|
import { logger } from './logger.mjs';
|
|
5
|
+
export { atomicWrite, isSafeVaultName } from './vault-files.mjs';
|
|
6
|
+
import { atomicWrite, isSafeVaultName } from './vault-files.mjs';
|
|
5
7
|
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// Dots are permitted in the interior because repository names legitimately
|
|
10
|
-
// contain them — any domain-style name (`example.com`) produces a slug with a
|
|
11
|
-
// dot on ingest, and the stricter rule rejected thousands of real nodes over an
|
|
12
|
-
// ordinary character. What enables traversal is a path separator or `..`, and
|
|
13
|
-
// neither is reachable here — separators are outside the character class, a
|
|
14
|
-
// leading dot or dash is excluded by the first-character rule, and `..` is
|
|
15
|
-
// rejected outright below.
|
|
16
|
-
const SAFE_NAME = /^[a-zA-Z0-9_][a-zA-Z0-9_.-]*$/;
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* True when `name` is safe to use as a path segment.
|
|
20
|
-
*/
|
|
21
|
-
export function isSafeVaultName(name) {
|
|
22
|
-
const value = String(name);
|
|
23
|
-
if (!SAFE_NAME.test(value)) return false;
|
|
24
|
-
if (value.includes('..')) return false;
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Atomic file write using write-then-rename to prevent partial corruption.
|
|
30
|
-
*/
|
|
31
|
-
export function atomicWrite(filePath, content) {
|
|
32
|
-
const tmpPath = `${filePath}.tmp.${process.pid}`;
|
|
33
|
-
fs.writeFileSync(tmpPath, content, 'utf8');
|
|
34
|
-
fs.renameSync(tmpPath, filePath);
|
|
35
|
-
}
|
|
8
|
+
// Re-export pure path/atomic-write helpers for existing callers. Local retrieval
|
|
9
|
+
// imports vault-files directly so it never loads logger/configuration/auth.
|
|
36
10
|
|
|
37
11
|
/**
|
|
38
12
|
* Deep-strip undefined values from an object so js-yaml (via gray-matter)
|
package/src/server/index.mjs
CHANGED
|
@@ -289,7 +289,7 @@ app.get('/health', requireAuthOrLocal, async (req, res) => {
|
|
|
289
289
|
// No agent CLIs on PATH is normal for a headless server brain (a droplet has
|
|
290
290
|
// none), so it is reported in cli_agents but does not make the brain
|
|
291
291
|
// "degraded" — that status is for things that break memory itself.
|
|
292
|
-
const hasCriticalIssue = emergencyAlerts.length > 0 ||
|
|
292
|
+
const hasCriticalIssue = emergencyAlerts.length > 0 || ['dead', 'conflict', 'unknown'].includes(daemonStatus)
|
|
293
293
|
|| unembeddedBrains.length > 0;
|
|
294
294
|
|
|
295
295
|
// Check Caddy and Cloudflare status
|
|
@@ -402,6 +402,18 @@ try {
|
|
|
402
402
|
|
|
403
403
|
// ─── API Routes (/v1/chat/completions) ──────────────────────────────────────────
|
|
404
404
|
|
|
405
|
+
// The single agent entry point. Mounted separately from apiRouter so a failure
|
|
406
|
+
// in one surface cannot leave the other dark.
|
|
407
|
+
try {
|
|
408
|
+
const { invokeRouter } = await import('./routes/invoke.mjs');
|
|
409
|
+
if (invokeRouter) {
|
|
410
|
+
app.use(invokeRouter);
|
|
411
|
+
logger.info('server', 'Agent invoke endpoint mounted at /v1/invoke');
|
|
412
|
+
}
|
|
413
|
+
} catch (err) {
|
|
414
|
+
logger.error('server', `invoke router failed to load; /v1/invoke left unmounted: ${err.message}`);
|
|
415
|
+
}
|
|
416
|
+
|
|
405
417
|
try {
|
|
406
418
|
const { apiRouter } = await import('./api.mjs');
|
|
407
419
|
if (apiRouter) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Full developer adapter: provider/credential work lives in a cancellable group. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { pathToFileURL } from 'node:url';
|
|
4
|
+
|
|
5
|
+
// Keep dependency diagnostics out of the structured result pipe. The parent
|
|
6
|
+
// never returns diagnostics, including those emitted by installed owner code.
|
|
7
|
+
console.log = console.info = console.debug = (...args) => console.error(...args);
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
let input = '';
|
|
11
|
+
for await (const chunk of process.stdin) {
|
|
12
|
+
input += chunk;
|
|
13
|
+
if (Buffer.byteLength(input) > 32768) throw new Error('Input limit');
|
|
14
|
+
}
|
|
15
|
+
const { operation, params, brain, timeoutMs } = JSON.parse(input);
|
|
16
|
+
if (!['decision.run', 'decision.predict'].includes(operation)) throw new Error('Unknown operation');
|
|
17
|
+
const plugin = path.join(brain, 'plugins/decision');
|
|
18
|
+
const client = await import(pathToFileURL(path.join(plugin, 'src/client.mjs')).href);
|
|
19
|
+
const auth = await import(pathToFileURL(path.join(plugin, 'auth-client.mjs')).href);
|
|
20
|
+
const apiKey = auth.readAuthSecret('OPENROUTER_API_KEY');
|
|
21
|
+
if (!apiKey) {
|
|
22
|
+
process.stdout.write(JSON.stringify({ ok: false, error: 'credential-unavailable' }));
|
|
23
|
+
} else {
|
|
24
|
+
let result;
|
|
25
|
+
if (operation === 'decision.run') {
|
|
26
|
+
const d = await client.requestDecision({ apiKey, model: params.model || client.DEFAULT_MODEL,
|
|
27
|
+
endpoint: client.DEFAULT_ENDPOINT, timeoutMs, state: params.state || {}, questions: params.questions });
|
|
28
|
+
if (d) {
|
|
29
|
+
const answers = Object.fromEntries(Object.entries(params.questions).map(([key, question]) => [key,
|
|
30
|
+
question?.type === 'noul' ? d.noul(key) : question?.type === 'score' ? d.score(key) : d.choice(key)]));
|
|
31
|
+
result = { answered: true, answers, model: d.modelVersion, cost_usd: d.cost, latency_ms: d.latencyMs };
|
|
32
|
+
}
|
|
33
|
+
} else {
|
|
34
|
+
const { predictNextAction } = await import('../core/action-predictor.mjs');
|
|
35
|
+
result = await predictNextAction(params.intent || '', {
|
|
36
|
+
state: params.state || {}, apiKey, requestDecision: client.requestDecision,
|
|
37
|
+
derivedDir: path.join(brain, 'memory-derived'), projectRoot: process.cwd(),
|
|
38
|
+
model: client.DEFAULT_MODEL, endpoint: client.DEFAULT_ENDPOINT, timeoutMs,
|
|
39
|
+
limit: Math.max(5, Math.min(40, Number(params.limit) || 20)),
|
|
40
|
+
});
|
|
41
|
+
if (result?.ok === false && result.reason === 'no-decision') result = null;
|
|
42
|
+
}
|
|
43
|
+
process.stdout.write(JSON.stringify(result ? { ok: true, result } : { ok: false, error: 'provider-failed' }));
|
|
44
|
+
}
|
|
45
|
+
} catch {
|
|
46
|
+
// Exception text can contain provider payloads, credentials or private paths.
|
|
47
|
+
process.stdout.write(JSON.stringify({ ok: false, error: 'decision-failed' }));
|
|
48
|
+
process.exitCode = 1;
|
|
49
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
export const INVOKE_LIMITS = Object.freeze({ timeoutMs: 60000, outputBytes: 262144, concurrency: 4, perKey: 1 });
|
|
5
|
+
|
|
6
|
+
export class InvokeError extends Error {
|
|
7
|
+
constructor(status, code, message, result = null) {
|
|
8
|
+
super(message); this.status = status; this.code = code; this.result = result;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Deliberate allowlist: no tokens, NODE_OPTIONS, preload hooks or secret-store selectors. */
|
|
13
|
+
export function invokeEnvironment(full, source = process.env) {
|
|
14
|
+
const env = { FORCE_COLOR: '0', NO_COLOR: '1' };
|
|
15
|
+
for (const name of ['PATH', 'LANG', 'LC_ALL', 'TZ', 'TMPDIR', 'SystemRoot']) {
|
|
16
|
+
if (source[name]) env[name] = source[name];
|
|
17
|
+
}
|
|
18
|
+
// Full mode is intentionally trusted host administration, including the auth
|
|
19
|
+
// owner's credential resolution. Ambient API keys still are not inherited.
|
|
20
|
+
if (full) for (const name of ['HOME', 'USER', 'AGENT_DIR', 'TR_SECRETS_BRAIN', 'TR_SECRETS_PASSWORD']) {
|
|
21
|
+
if (source[name]) env[name] = source[name];
|
|
22
|
+
}
|
|
23
|
+
return env;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Bounded direct-argv child; cancellation kills its POSIX process group. */
|
|
27
|
+
export async function executeInvokeChild({ entry, args = [], cwd, env, input, timeoutMs, signal,
|
|
28
|
+
limits = INVOKE_LIMITS, nodeArgs = [] }) {
|
|
29
|
+
if (process.platform === 'win32') throw new InvokeError(503, 'unsupported-platform', 'Process-group enforcement is unavailable on this platform.');
|
|
30
|
+
const timeout = timeoutMs === undefined ? limits.timeoutMs : Number(timeoutMs);
|
|
31
|
+
if (!Number.isFinite(timeout) || timeout <= 0) throw new InvokeError(400, 'invalid-timeout', 'timeoutMs must be positive and finite.');
|
|
32
|
+
if (signal?.aborted) throw new InvokeError(403, 'authority-ended', 'Caller authority ended before execution.');
|
|
33
|
+
return await new Promise((resolve, reject) => {
|
|
34
|
+
const child = spawn(process.execPath, [...nodeArgs, entry, ...args], {
|
|
35
|
+
cwd, env, shell: false, detached: true, stdio: ['pipe', 'pipe', 'pipe'],
|
|
36
|
+
});
|
|
37
|
+
const chunks = { stdout: [], stderr: [] };
|
|
38
|
+
let bytes = 0, reason = null;
|
|
39
|
+
const kill = why => {
|
|
40
|
+
reason ||= why;
|
|
41
|
+
if (child.pid) try { process.kill(-child.pid, 'SIGKILL'); } catch (err) {
|
|
42
|
+
if (err.code !== 'ESRCH') child.kill('SIGKILL');
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
const abort = () => kill('authority-ended');
|
|
46
|
+
const timer = setTimeout(() => kill('timeout'), Math.min(timeout, limits.timeoutMs));
|
|
47
|
+
signal?.addEventListener('abort', abort, { once: true });
|
|
48
|
+
if (signal?.aborted) abort();
|
|
49
|
+
for (const channel of ['stdout', 'stderr']) child[channel].on('data', chunk => {
|
|
50
|
+
const remaining = Math.max(0, limits.outputBytes - bytes);
|
|
51
|
+
if (remaining) chunks[channel].push(chunk.subarray(0, remaining));
|
|
52
|
+
bytes += chunk.length;
|
|
53
|
+
if (bytes > limits.outputBytes) kill('output-limit');
|
|
54
|
+
});
|
|
55
|
+
child.stdin.on('error', () => {}); // An early exit may close stdin first.
|
|
56
|
+
child.stdin.end(input || '');
|
|
57
|
+
child.once('error', () => {
|
|
58
|
+
clearTimeout(timer); signal?.removeEventListener('abort', abort);
|
|
59
|
+
reject(new InvokeError(503, 'spawn-failed', 'Execution could not start.'));
|
|
60
|
+
});
|
|
61
|
+
child.once('close', (code, terminationSignal) => {
|
|
62
|
+
// Also retire descendants after a normally exiting parent.
|
|
63
|
+
if (child.pid) try { process.kill(-child.pid, 'SIGKILL'); } catch {}
|
|
64
|
+
clearTimeout(timer); signal?.removeEventListener('abort', abort);
|
|
65
|
+
resolve({ exit_code: code, signal: terminationSignal, stdout: Buffer.concat(chunks.stdout).toString(),
|
|
66
|
+
stderr: Buffer.concat(chunks.stderr).toString(), truncated: bytes > limits.outputBytes,
|
|
67
|
+
timed_out: reason === 'timeout', termination_reason: reason });
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function requireSuccessfulRun(run) {
|
|
73
|
+
if (run.exit_code !== 0 || run.termination_reason || run.truncated) {
|
|
74
|
+
const code = run.termination_reason || 'nonzero-exit';
|
|
75
|
+
throw new InvokeError(code === 'timeout' ? 504 : code === 'authority-ended' ? 403 : 422,
|
|
76
|
+
code, 'Execution did not complete successfully.', run);
|
|
77
|
+
}
|
|
78
|
+
return run;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function absoluteTarget(target, fallback) {
|
|
82
|
+
if (target === undefined) return fallback;
|
|
83
|
+
if (typeof target !== 'string' || !path.isAbsolute(target) || target.includes('\0')) {
|
|
84
|
+
throw new InvokeError(400, 'invalid-target', 'Full developer target must be an absolute working directory.');
|
|
85
|
+
}
|
|
86
|
+
return target;
|
|
87
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { InvokeError } from './invoke-execution.mjs';
|
|
2
|
+
import { validateConfinedFact } from '../core/confined-memory.mjs';
|
|
3
|
+
|
|
4
|
+
/** Resolve fresh key AND ancestors through the shared auth owner on each check. */
|
|
5
|
+
export function invokeAuthority(req, { loadKeys, keyHasScope }) {
|
|
6
|
+
try { return resolveAuthority(req, { loadKeys, keyHasScope }); }
|
|
7
|
+
catch (err) {
|
|
8
|
+
if (err instanceof InvokeError) throw err;
|
|
9
|
+
throw new InvokeError(503, 'authority-unavailable', 'Shared auth authority is unavailable.');
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolveAuthority(req, { loadKeys, keyHasScope }) {
|
|
14
|
+
if (req.auth?.type !== 'pat' || !req.auth.id) {
|
|
15
|
+
throw new InvokeError(403, 'explicit-key-required', 'Invoke requires a named PAT with an explicit invoke mode.');
|
|
16
|
+
}
|
|
17
|
+
const keys = loadKeys();
|
|
18
|
+
const chain = [], seen = new Set();
|
|
19
|
+
let key = keys.find(k => k.id === req.auth.id);
|
|
20
|
+
while (key) {
|
|
21
|
+
if (seen.has(key.id) || !keyHasScope(key, null)) throw new InvokeError(403, 'authority-ended', 'Key or issuer is expired, revoked or invalid.');
|
|
22
|
+
seen.add(key.id); chain.push(key);
|
|
23
|
+
if (key.issued_by === 'local-operator' || !key.issued_by) break;
|
|
24
|
+
key = keys.find(k => k.id === key.issued_by);
|
|
25
|
+
if (!key) throw new InvokeError(403, 'authority-ended', 'Issuer is unavailable.');
|
|
26
|
+
}
|
|
27
|
+
if (!chain.length) throw new InvokeError(403, 'authority-ended', 'Key is unavailable.');
|
|
28
|
+
const allows = scope => chain.every(k => keyHasScope(k, scope));
|
|
29
|
+
// The leaf must carry a literal mode. A historical wildcard does not opt in.
|
|
30
|
+
const full = chain.every(k => k.scopes?.includes('invoke:full')) && allows('invoke:full');
|
|
31
|
+
const restricted = chain[0].scopes?.includes('invoke:restricted') && allows('invoke:restricted');
|
|
32
|
+
if (!full && !restricted) throw new InvokeError(403, 'invoke-grant-required', 'Explicit invoke:full or invoke:restricted grant required; legacy keys are not migrated automatically.');
|
|
33
|
+
return { id: chain[0].id, scopes: chain[0].scopes, mode: full ? 'full' : 'restricted', allows };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function operationAllowed(authority, operation, scope, targets = ['server']) {
|
|
37
|
+
if (scope && !authority.allows(scope)) return false;
|
|
38
|
+
if (operation === 'capabilities') return true;
|
|
39
|
+
if (authority.mode === 'full') return true;
|
|
40
|
+
return ['ssss.read', 'ssss.write'].includes(operation) && targets.some(target =>
|
|
41
|
+
authority.allows(target === 'server' ? 'brain:server' : `brain:project-${target.slice('project:'.length)}`));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function authorizeInvoke(authority, operation, scope, params, targets = ['server']) {
|
|
45
|
+
if (scope && !authority.allows(scope)) throw new InvokeError(403, 'insufficient-scope', `Operation requires scope '${scope}'.`);
|
|
46
|
+
if (!operationAllowed(authority, operation, scope, targets)) {
|
|
47
|
+
throw new InvokeError(403, 'unsupported-restricted-operation', 'No confined owner adapter is available for this operation.');
|
|
48
|
+
}
|
|
49
|
+
if (authority.mode === 'restricted') {
|
|
50
|
+
if (operation === 'capabilities') return;
|
|
51
|
+
if (!targets.includes(params.target ?? 'server') || params.scope !== undefined) {
|
|
52
|
+
throw new InvokeError(403, 'target-denied', 'Restricted memory requires an available granted brain target.');
|
|
53
|
+
}
|
|
54
|
+
if (operation === 'ssss.write') {
|
|
55
|
+
try { validateConfinedFact({ ...params, target: 'server' }); }
|
|
56
|
+
catch { throw new InvokeError(400, 'invalid-fact', 'Restricted writes accept only fact content, optional title and plain tags.'); }
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const allowed = new Set(['query', 'category', 'limit', 'target', 'timeoutMs']);
|
|
60
|
+
if (Object.keys(params).some(k => !allowed.has(k))) {
|
|
61
|
+
throw new InvokeError(400, 'unknown-parameter', 'Unsupported restricted retrieval parameter.');
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** Trusted read-only adapter: no CLI bootstrap, auth, plugins or provider imports. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { fastSearch } from '../core/fast-recall.mjs';
|
|
4
|
+
|
|
5
|
+
try {
|
|
6
|
+
let input = '';
|
|
7
|
+
for await (const chunk of process.stdin) {
|
|
8
|
+
input += chunk;
|
|
9
|
+
if (Buffer.byteLength(input) > 16384) throw new Error('Input limit exceeded');
|
|
10
|
+
}
|
|
11
|
+
const { brain, query, category, limit } = JSON.parse(input);
|
|
12
|
+
const nodes = fastSearch(query, {
|
|
13
|
+
vaultDir: path.join(brain, 'memory-vault'),
|
|
14
|
+
derivedDir: path.join(brain, 'memory-derived'),
|
|
15
|
+
top_k: limit, category, fullText: true,
|
|
16
|
+
});
|
|
17
|
+
if (nodes.stats.index !== 'local') throw new Error(`Local index ${nodes.stats.index}; compile the authorized brain first`);
|
|
18
|
+
process.stdout.write(JSON.stringify({ count: nodes.length, nodes: nodes.map(n => ({
|
|
19
|
+
slug: n.slug, category: n.category, title: n.title, status: n.status,
|
|
20
|
+
score: n.score, snippet: String(n.body || '').slice(0, 240),
|
|
21
|
+
})), freshness: nodes.stats.freshness }));
|
|
22
|
+
} catch {
|
|
23
|
+
// Never include paths, memory payloads or parser input in the failure channel.
|
|
24
|
+
process.stderr.write('Authorized local retrieval failed; check its index.');
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Resolve operator registrations; caller inputs are IDs, never host paths. */
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { readRegisteredProjects, readBrainToggles } from '../core/brain-registry.mjs';
|
|
5
|
+
import { readProjectId } from '../core/project-id.mjs';
|
|
6
|
+
import { InvokeError } from './invoke-execution.mjs';
|
|
7
|
+
|
|
8
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
9
|
+
export const projectBrainScope = id => `brain:project-${id}`;
|
|
10
|
+
|
|
11
|
+
function safeMemoryRoots(brain) {
|
|
12
|
+
for (const relative of ['memory-vault', 'memory-derived', 'memory-derived/local-search.json', 'memory-derived/memory-layers.jsonl']) {
|
|
13
|
+
const file = path.join(brain, relative);
|
|
14
|
+
try { if (fs.lstatSync(file).isSymbolicLink() || fs.realpathSync(file) !== file) return false; }
|
|
15
|
+
catch (err) { if (err.code !== 'ENOENT') return false; }
|
|
16
|
+
}
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function restrictedBrainTargets(authority, { serverBrain, registryBrain }) {
|
|
21
|
+
const targets = new Map();
|
|
22
|
+
if (authority.allows('brain:server')) targets.set('server', fs.realpathSync(serverBrain));
|
|
23
|
+
// A plain server grant does not require unrelated registry availability.
|
|
24
|
+
if (!authority.scopes?.some(scope => scope === '*' || scope === 'brain:*' || /^brain:project-/.test(scope))) return targets;
|
|
25
|
+
let rows, toggles;
|
|
26
|
+
try {
|
|
27
|
+
rows = readRegisteredProjects(registryBrain);
|
|
28
|
+
toggles = readBrainToggles(registryBrain, { strict: true });
|
|
29
|
+
} catch { throw new InvokeError(503, 'target-registry-unavailable', 'Project target metadata is unavailable.'); }
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
for (const row of rows) {
|
|
32
|
+
const id = row.project_id;
|
|
33
|
+
if (typeof id !== 'string' || !UUID.test(id) || !authority.allows(projectBrainScope(id))) continue;
|
|
34
|
+
if (seen.has(id)) throw new InvokeError(503, 'ambiguous-project-target', 'A granted project identity is registered more than once.');
|
|
35
|
+
seen.add(id);
|
|
36
|
+
try {
|
|
37
|
+
if (typeof row.path !== 'string' || !path.isAbsolute(row.path) || typeof row.brainDir !== 'string' || !path.isAbsolute(row.brainDir)) continue;
|
|
38
|
+
const expected = path.join(fs.realpathSync(row.path), '.agent', 'skills', 'total-recall');
|
|
39
|
+
const brain = fs.realpathSync(row.brainDir);
|
|
40
|
+
// Nested links cannot substitute another project or global brain.
|
|
41
|
+
if (brain !== expected || fs.realpathSync(path.join(brain, 'config')) !== path.join(brain, 'config') ||
|
|
42
|
+
!fs.lstatSync(path.join(brain, 'config', 'brain.json')).isFile() ||
|
|
43
|
+
fs.lstatSync(path.join(brain, 'config', 'brain.json')).isSymbolicLink() || readProjectId(brain) !== id || !safeMemoryRoots(brain)) continue;
|
|
44
|
+
if (toggles[path.resolve(row.brainDir)] === false || toggles[brain] === false) continue;
|
|
45
|
+
targets.set(`project:${id}`, brain);
|
|
46
|
+
} catch { /* A missing, unsafe or mismatched registration grants nothing. */ }
|
|
47
|
+
}
|
|
48
|
+
return targets;
|
|
49
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Structured trusted mutation entrypoint. Never accepts executable code. */
|
|
2
|
+
import { createConfinedFact } from '../core/confined-memory.mjs';
|
|
3
|
+
|
|
4
|
+
try {
|
|
5
|
+
let input = '';
|
|
6
|
+
for await (const chunk of process.stdin) {
|
|
7
|
+
input += chunk;
|
|
8
|
+
if (Buffer.byteLength(input) > 16384) throw new Error('Input limit');
|
|
9
|
+
}
|
|
10
|
+
const { brain, params, caller } = JSON.parse(input);
|
|
11
|
+
try { process.stdout.write(JSON.stringify(await createConfinedFact(brain, params, caller))); }
|
|
12
|
+
catch (err) {
|
|
13
|
+
if (err.result) process.stdout.write(JSON.stringify(err.result));
|
|
14
|
+
throw err;
|
|
15
|
+
}
|
|
16
|
+
} catch {
|
|
17
|
+
process.stderr.write('Confined fact creation did not complete; check committed/indexed status before retry.');
|
|
18
|
+
process.exitCode = 1;
|
|
19
|
+
}
|