total-recall-brain 3.28.2 → 3.30.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/bin/total-recall.mjs +24 -1
- package/docs/ARCHITECTURE.md +26 -18
- package/extension/background.js +259 -84
- package/extension/content-script.css +7 -13
- package/extension/content-script.js +252 -477
- package/extension/icons/icon-128.png +0 -0
- package/extension/icons/icon-16.png +0 -0
- package/extension/icons/icon-32.png +0 -0
- package/extension/icons/icon-48.png +0 -0
- package/extension/icons/mark.svg +6 -0
- package/extension/lib/brain-client.js +160 -73
- package/extension/lib/tokens.css +187 -0
- package/extension/lib/ui-helpers.js +229 -0
- package/extension/lib/ui-helpers.spec.mjs +94 -0
- package/extension/manifest.json +20 -5
- package/extension/options/options.css +64 -218
- package/extension/options/options.html +91 -43
- package/extension/options/options.js +208 -85
- package/extension/sidepanel/sidepanel.css +268 -970
- package/extension/sidepanel/sidepanel.html +200 -166
- package/extension/sidepanel/sidepanel.js +788 -942
- package/frontend/dist/assets/index-DGMNZi_1.js +5742 -0
- package/frontend/dist/index.html +1 -1
- package/frontend/src/api/plugins.ts +124 -92
- package/frontend/src/components/ResearchAgendaTab.spec.tsx +11 -0
- package/frontend/src/components/ResearchAgendaTab.tsx +19 -14
- package/frontend/src/pages/PluginsPage.spec.tsx +87 -16
- package/frontend/src/pages/PluginsPage.tsx +454 -1218
- package/package.json +2 -1
- package/plugins/code-quality/README.md +25 -0
- package/plugins/code-quality/cli.mjs +25 -0
- package/plugins/code-quality/plugin.json +17 -0
- package/plugins/git-sentinel/README.md +27 -0
- package/{scaffold/.agent/plugins → plugins}/git-sentinel/cli.mjs +8 -5
- package/plugins/git-sentinel/plugin.json +18 -0
- package/plugins/system-monitor/README.md +26 -0
- package/{scaffold/.agent/plugins → plugins}/system-monitor/cli.mjs +5 -4
- package/{scaffold/.agent/plugins → plugins}/system-monitor/generator.mjs +4 -5
- package/plugins/system-monitor/plugin.json +29 -0
- package/scaffold/.agent/skills/meta-harness/SKILL.md +4 -4
- package/scaffold/.agent/skills/plugins/SKILL.md +53 -0
- package/scaffold/.agent/skills/total-recall/SKILL.md +22 -6
- package/scaffold/.agent/skills/total-recall/scripts/brain-state.json +1 -0
- package/src/cli/agent.mjs +1 -1
- package/src/cli/app/index.mjs +493 -0
- package/src/cli/brain.mjs +33 -3
- package/src/cli/command.mjs +80 -41
- package/src/cli/connect.mjs +10 -1
- package/src/cli/harness.mjs +1 -1
- package/src/cli/init.mjs +3 -1
- package/src/cli/mesh.mjs +118 -12
- package/src/cli/plugin/create.mjs +144 -25
- package/src/cli/plugin/index.mjs +83 -53
- package/src/cli/plugin/install.mjs +10 -127
- package/src/cli/plugin/list.mjs +44 -44
- package/src/cli/plugin/remove.mjs +23 -30
- package/src/cli/plugin/scaffold-cmd.mjs +30 -0
- package/src/cli/plugin/search.mjs +108 -23
- package/src/cli/rebuild.mjs +37 -1
- package/src/cli/recall.mjs +17 -2
- package/src/cli/research.mjs +4 -6
- package/src/cli/share.mjs +2 -0
- package/src/core/app-deploy/apply.mjs +386 -0
- package/src/core/app-deploy/design-tokens.mjs +217 -0
- package/src/core/app-deploy/plan.mjs +249 -0
- package/src/core/app-deploy/resolve.mjs +295 -0
- package/src/core/app-deploy/source.mjs +337 -0
- package/src/core/app-deploy/standalone.mjs +248 -0
- package/src/core/app-deploy/upgrade.mjs +256 -0
- package/src/core/app-deploy/verify.mjs +213 -0
- package/src/core/bound-hosts.mjs +1 -1
- package/src/core/brain-registry.mjs +146 -0
- package/src/core/brain-state.json +1 -0
- package/src/core/daemon-loop.mjs +30 -26
- package/src/core/dream.mjs +5 -17
- package/src/core/fact-seeker.mjs +53 -368
- package/src/core/lan-discovery.mjs +3 -3
- package/src/core/mesh-access-sync.mjs +134 -0
- package/src/core/mesh-access.mjs +18 -3
- package/src/core/mesh-discover.mjs +173 -0
- package/src/core/mesh.mjs +131 -32
- package/src/core/optimizer.mjs +4 -41
- package/src/core/plugin-bundle.mjs +167 -0
- package/src/core/plugin-context.mjs +8 -13
- package/src/core/plugin-loader.mjs +272 -68
- package/src/core/plugin-peers.mjs +163 -0
- package/src/core/plugin-public.mjs +85 -0
- package/src/core/plugin-runner-child.mjs +24 -0
- package/src/core/plugin-runner.mjs +102 -0
- package/src/core/plugin-store.mjs +384 -0
- package/src/core/plugin-tasks.mjs +212 -0
- package/src/core/post-mortem.mjs +40 -9
- package/src/core/research-gate.mjs +182 -0
- package/src/core/research-queue.mjs +47 -7
- package/src/core/research-surface.mjs +211 -0
- package/src/core/research.mjs +25 -32
- package/src/core/scheduler.mjs +67 -153
- package/src/core/schema.mjs +129 -0
- package/src/core/search.mjs +12 -1
- package/src/core/secret-integration-research.mjs +12 -8
- package/src/core/secrets-store.mjs +76 -2
- package/src/core/session-watcher.mjs +6 -0
- package/src/core/surface.mjs +64 -3
- package/src/core/task-executors.mjs +8 -45
- package/src/core/vault-cache.mjs +70 -4
- package/src/server/api.mjs +21 -0
- package/src/server/rest.mjs +2 -0
- package/src/server/route-manifest.json +21 -5
- package/src/server/routes/collab.mjs +38 -11
- package/src/server/routes/extension.mjs +3 -2
- package/src/server/routes/memory.mjs +24 -2
- package/src/server/routes/mesh.mjs +7 -7
- package/src/server/routes/plugins-mesh.mjs +49 -0
- package/src/server/routes/plugins.mjs +91 -426
- package/src/server/routes/research.mjs +5 -2
- package/src/server/routes/share.mjs +2 -0
- package/src/server/tools.mjs +29 -2
- package/extension/icons/README.md +0 -27
- package/extension/lib/preconfigured.js +0 -5
- package/extension/popup/popup.css +0 -227
- package/extension/popup/popup.html +0 -50
- package/extension/popup/popup.js +0 -175
- package/frontend/dist/assets/index-U7o-qU99.js +0 -5742
- package/scaffold/.agent/plugins/.gitkeep +0 -0
- package/scaffold/.agent/plugins/code-quality/README.md +0 -16
- package/scaffold/.agent/plugins/code-quality/cli.mjs +0 -15
- package/scaffold/.agent/plugins/code-quality/plugin.json +0 -34
- package/scaffold/.agent/plugins/git-sentinel/README.md +0 -16
- package/scaffold/.agent/plugins/git-sentinel/plugin.json +0 -34
- package/scaffold/.agent/plugins/meta-harness/README.md +0 -16
- package/scaffold/.agent/plugins/meta-harness/cli.mjs +0 -34
- package/scaffold/.agent/plugins/meta-harness/plugin.json +0 -34
- package/scaffold/.agent/plugins/system-monitor/README.md +0 -16
- package/scaffold/.agent/plugins/system-monitor/plugin.json +0 -38
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Capability Deployment — Source Resolver & Integrity Verifier
|
|
3
|
+
*
|
|
4
|
+
* Implements Phase 2 of CAPABILITY_DEPLOYMENT_PLUGINS:
|
|
5
|
+
* - Pinned source/hash verification
|
|
6
|
+
* - Safe extraction and filesystem containment
|
|
7
|
+
* - Strict rejection of symlinks, parent directory traversals, and absolute paths
|
|
8
|
+
* - Prevention of secret and environment credential leakage (.env, *.key, *.pem, secrets.enc)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import crypto from 'node:crypto';
|
|
15
|
+
import {
|
|
16
|
+
validatePluginManifest,
|
|
17
|
+
bundledPluginsDir,
|
|
18
|
+
getPlugin
|
|
19
|
+
} from '../plugin-loader.mjs';
|
|
20
|
+
import {
|
|
21
|
+
decodeBundle,
|
|
22
|
+
hashPluginDir
|
|
23
|
+
} from '../plugin-bundle.mjs';
|
|
24
|
+
import {
|
|
25
|
+
parsePublicPluginSource,
|
|
26
|
+
fetchPublicBundle
|
|
27
|
+
} from '../plugin-public.mjs';
|
|
28
|
+
|
|
29
|
+
export const PROHIBITED_SECRET_PATTERNS = [
|
|
30
|
+
/^\.env(\..+)?$/i,
|
|
31
|
+
/\.pem$/i,
|
|
32
|
+
/\.key$/i,
|
|
33
|
+
/\.pkcs12$/i,
|
|
34
|
+
/\.pfx$/i,
|
|
35
|
+
/^id_rsa(\..+)?$/i,
|
|
36
|
+
/^id_ed25519(\..+)?$/i,
|
|
37
|
+
/^secrets\.enc$/i,
|
|
38
|
+
/credentials\.json$/i,
|
|
39
|
+
/client_secret.*\.json$/i
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Checks if a relative file path matches known credential/secret patterns.
|
|
44
|
+
* @param {string} relPath
|
|
45
|
+
* @returns {boolean}
|
|
46
|
+
*/
|
|
47
|
+
export function isProhibitedSecretFile(relPath) {
|
|
48
|
+
const basename = path.basename(relPath);
|
|
49
|
+
return PROHIBITED_SECRET_PATTERNS.some((pattern) => pattern.test(basename));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Validates that a path is safe and strictly relative.
|
|
54
|
+
* Rejects absolute paths, windows drive letters, null bytes, backslashes, and directory traversals.
|
|
55
|
+
* @param {string} p
|
|
56
|
+
*/
|
|
57
|
+
export function assertSafeSourcePath(p) {
|
|
58
|
+
if (typeof p !== 'string' || p.length === 0 || p.length > 512) {
|
|
59
|
+
throw new Error(`Invalid source path: empty or exceeds 512 characters (${p})`);
|
|
60
|
+
}
|
|
61
|
+
if (p.includes('\\') || p.includes('\0')) {
|
|
62
|
+
throw new Error(`Security error: path '${p}' contains forbidden characters (backslash or null byte)`);
|
|
63
|
+
}
|
|
64
|
+
if (p.startsWith('/') || /^[A-Za-z]:/.test(p)) {
|
|
65
|
+
throw new Error(`Security error: path '${p}' must be relative, not absolute`);
|
|
66
|
+
}
|
|
67
|
+
const parts = p.split('/');
|
|
68
|
+
for (const part of parts) {
|
|
69
|
+
if (part === '' || part === '.' || part === '..') {
|
|
70
|
+
throw new Error(`Security error: path '${p}' contains illegal traversal component ('${part}')`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Canonical SHA-256 computation over entries sorted by path.
|
|
77
|
+
* Format: path \0 length \0 data
|
|
78
|
+
* @param {Array<{ path: string, data: Buffer }>} entries
|
|
79
|
+
* @returns {string} hex digest
|
|
80
|
+
*/
|
|
81
|
+
export function computeEntriesHash(entries) {
|
|
82
|
+
const sorted = [...entries].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
83
|
+
const h = crypto.createHash('sha256');
|
|
84
|
+
for (const e of sorted) {
|
|
85
|
+
h.update(e.path);
|
|
86
|
+
h.update('\0');
|
|
87
|
+
h.update(String(e.data.length));
|
|
88
|
+
h.update('\0');
|
|
89
|
+
h.update(e.data);
|
|
90
|
+
}
|
|
91
|
+
return h.digest('hex');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Scans a source directory, strictly verifying safety and computing canonical hash.
|
|
96
|
+
* @param {string} dirPath
|
|
97
|
+
* @returns {{ sha256: string, entries: Array<{ path: string, abs: string, size: number, mode: number, data: Buffer }> }}
|
|
98
|
+
*/
|
|
99
|
+
export function scanAndVerifyDirectory(dirPath) {
|
|
100
|
+
const root = fs.realpathSync(dirPath);
|
|
101
|
+
const entries = [];
|
|
102
|
+
const SKIP_DIRS = new Set(['.git', 'node_modules', '.agent']);
|
|
103
|
+
|
|
104
|
+
function walk(current) {
|
|
105
|
+
for (const item of fs.readdirSync(current, { withFileTypes: true })) {
|
|
106
|
+
if (item.name.startsWith('.') && item.name !== '.agent') {
|
|
107
|
+
// Skip hidden dotfiles, but flag prohibited .env files
|
|
108
|
+
if (isProhibitedSecretFile(item.name)) {
|
|
109
|
+
throw new Error(`Security error: Prohibited credential file '${item.name}' found in capability source`);
|
|
110
|
+
}
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (SKIP_DIRS.has(item.name)) continue;
|
|
114
|
+
|
|
115
|
+
const abs = path.join(current, item.name);
|
|
116
|
+
const rel = path.relative(root, abs).split(path.sep).join('/');
|
|
117
|
+
|
|
118
|
+
assertSafeSourcePath(rel);
|
|
119
|
+
|
|
120
|
+
if (isProhibitedSecretFile(rel)) {
|
|
121
|
+
throw new Error(`Security error: Prohibited credential file '${rel}' found in capability source`);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (item.isSymbolicLink()) {
|
|
125
|
+
throw new Error(`Security error: Symbolic link detected at '${rel}'. Symlinks are forbidden in capability sources.`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (item.isDirectory()) {
|
|
129
|
+
walk(abs);
|
|
130
|
+
} else if (item.isFile()) {
|
|
131
|
+
const stat = fs.statSync(abs);
|
|
132
|
+
const data = fs.readFileSync(abs);
|
|
133
|
+
entries.push({
|
|
134
|
+
path: rel,
|
|
135
|
+
abs,
|
|
136
|
+
size: stat.size,
|
|
137
|
+
mode: stat.mode & 0o777,
|
|
138
|
+
data
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
walk(root);
|
|
145
|
+
|
|
146
|
+
if (entries.length === 0) {
|
|
147
|
+
throw new Error(`Capability source directory is empty: ${dirPath}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const manifestEntry = entries.find((e) => e.path === 'plugin.json');
|
|
151
|
+
if (!manifestEntry) {
|
|
152
|
+
throw new Error(`Capability source missing required manifest 'plugin.json' at root: ${dirPath}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const sha256 = computeEntriesHash(entries);
|
|
156
|
+
return { sha256, entries };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Safely extracts validated entries into a target destination.
|
|
161
|
+
* @param {Array<{ path: string, mode?: number, data: Buffer }>} entries
|
|
162
|
+
* @param {string} destDir
|
|
163
|
+
*/
|
|
164
|
+
export function extractEntriesSafely(entries, destDir) {
|
|
165
|
+
if (!fs.existsSync(destDir)) {
|
|
166
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
167
|
+
}
|
|
168
|
+
const realDest = fs.realpathSync(destDir);
|
|
169
|
+
|
|
170
|
+
for (const entry of entries) {
|
|
171
|
+
assertSafeSourcePath(entry.path);
|
|
172
|
+
if (isProhibitedSecretFile(entry.path)) {
|
|
173
|
+
throw new Error(`Security error: cannot extract prohibited secret file '${entry.path}'`);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const targetAbs = path.join(realDest, entry.path);
|
|
177
|
+
// Ensure target doesn't escape destination directory
|
|
178
|
+
if (!targetAbs.startsWith(realDest + path.sep)) {
|
|
179
|
+
throw new Error(`Security error: extracted path '${entry.path}' escapes destination directory`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
fs.mkdirSync(path.dirname(targetAbs), { recursive: true });
|
|
183
|
+
fs.writeFileSync(targetAbs, entry.data, { mode: entry.mode || 0o644 });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Resolves, validates, and stages a capability plugin from a local path, bundled ID, public link, or bundle.
|
|
189
|
+
*
|
|
190
|
+
* @param {string|object} source - Local path, bundled id, public URL, or in-memory bundle
|
|
191
|
+
* @param {object} [options]
|
|
192
|
+
* @param {string} [options.expectedHash] - Expected SHA-256 hash
|
|
193
|
+
* @param {string} [options.projectRoot] - Target project root
|
|
194
|
+
* @param {string} [options.stagingDir] - Destination directory to stage files
|
|
195
|
+
* @param {object} [options.publicDeps] - Dependency injection for network operations (fetch)
|
|
196
|
+
* @returns {Promise<{
|
|
197
|
+
* id: string,
|
|
198
|
+
* version: string,
|
|
199
|
+
* manifest: object,
|
|
200
|
+
* sourceUri: string,
|
|
201
|
+
* sourceType: string,
|
|
202
|
+
* sha256: string,
|
|
203
|
+
* fileCount: number,
|
|
204
|
+
* sizeBytes: number,
|
|
205
|
+
* stagedDir: string,
|
|
206
|
+
* files: Array<{ path: string, size: number, sha256: string }>,
|
|
207
|
+
* cleanup: () => void
|
|
208
|
+
* }>}
|
|
209
|
+
*/
|
|
210
|
+
export async function resolveCapabilitySource(source, options = {}) {
|
|
211
|
+
let expectedHash = options.expectedHash || null;
|
|
212
|
+
let sourceType = 'local';
|
|
213
|
+
let sourceUri = typeof source === 'string' ? source : 'in-memory-bundle';
|
|
214
|
+
let entries = [];
|
|
215
|
+
let computedSha = null;
|
|
216
|
+
|
|
217
|
+
// 1. In-memory bundle object
|
|
218
|
+
if (typeof source === 'object' && source !== null && source.files && source.format) {
|
|
219
|
+
sourceType = 'bundle';
|
|
220
|
+
const decoded = decodeBundle(source, { expectedSha256: expectedHash || undefined });
|
|
221
|
+
computedSha = decoded.sha256;
|
|
222
|
+
entries = decoded.entries;
|
|
223
|
+
}
|
|
224
|
+
// 2. Public hash-pinned URL
|
|
225
|
+
else if (typeof source === 'string' && (source.startsWith('http://') || source.startsWith('https://'))) {
|
|
226
|
+
sourceType = 'public';
|
|
227
|
+
const parsed = parsePublicPluginSource(source);
|
|
228
|
+
expectedHash = expectedHash || parsed.sha256;
|
|
229
|
+
const { bundle, sha256 } = await fetchPublicBundle(source, options.publicDeps);
|
|
230
|
+
const decoded = decodeBundle(bundle, { expectedSha256: expectedHash });
|
|
231
|
+
computedSha = sha256;
|
|
232
|
+
entries = decoded.entries;
|
|
233
|
+
}
|
|
234
|
+
// 3. Bundled or local path / ID
|
|
235
|
+
else if (typeof source === 'string') {
|
|
236
|
+
let candidateDir = null;
|
|
237
|
+
|
|
238
|
+
if (source.startsWith('bundled:')) {
|
|
239
|
+
sourceType = 'bundled';
|
|
240
|
+
const pluginId = source.replace('bundled:', '');
|
|
241
|
+
candidateDir = path.join(bundledPluginsDir(), pluginId);
|
|
242
|
+
} else if (fs.existsSync(source)) {
|
|
243
|
+
sourceType = 'local';
|
|
244
|
+
candidateDir = path.resolve(source);
|
|
245
|
+
} else {
|
|
246
|
+
// Check if it's an installed or bundled plugin by ID
|
|
247
|
+
const installed = getPlugin(source, options.projectRoot || process.cwd());
|
|
248
|
+
if (installed && fs.existsSync(installed.dir)) {
|
|
249
|
+
sourceType = installed.scope === 'bundled' ? 'bundled' : 'local';
|
|
250
|
+
candidateDir = installed.dir;
|
|
251
|
+
} else {
|
|
252
|
+
const bundledDir = path.join(bundledPluginsDir(), source);
|
|
253
|
+
if (fs.existsSync(bundledDir)) {
|
|
254
|
+
sourceType = 'bundled';
|
|
255
|
+
candidateDir = bundledDir;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (!candidateDir || !fs.existsSync(candidateDir)) {
|
|
261
|
+
throw new Error(`Cannot resolve capability plugin source: '${source}' (path or plugin does not exist)`);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const scan = scanAndVerifyDirectory(candidateDir);
|
|
265
|
+
computedSha = scan.sha256;
|
|
266
|
+
entries = scan.entries;
|
|
267
|
+
} else {
|
|
268
|
+
throw new Error(`Invalid capability plugin source: ${typeof source}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Verify hash pin if provided
|
|
272
|
+
if (expectedHash && computedSha !== expectedHash) {
|
|
273
|
+
const err = new Error(
|
|
274
|
+
`Capability source hash mismatch: expected '${expectedHash}', but computed '${computedSha}'`
|
|
275
|
+
);
|
|
276
|
+
err.name = 'SourceHashMismatchError';
|
|
277
|
+
err.expected = expectedHash;
|
|
278
|
+
err.actual = computedSha;
|
|
279
|
+
throw err;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// Parse and validate plugin manifest
|
|
283
|
+
const manifestEntry = entries.find((e) => e.path === 'plugin.json');
|
|
284
|
+
if (!manifestEntry) {
|
|
285
|
+
throw new Error(`Capability source is missing 'plugin.json' manifest.`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
let manifest;
|
|
289
|
+
try {
|
|
290
|
+
manifest = JSON.parse(manifestEntry.data.toString('utf8'));
|
|
291
|
+
} catch (parseErr) {
|
|
292
|
+
throw new Error(`Malformed plugin.json manifest: ${parseErr.message}`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const validation = validatePluginManifest(manifest);
|
|
296
|
+
if (!validation.valid) {
|
|
297
|
+
throw new Error(`Invalid plugin manifest in source: ${validation.errors.join('; ')}`);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Stage extracted files
|
|
301
|
+
let stagedDir = options.stagingDir;
|
|
302
|
+
let isTemp = false;
|
|
303
|
+
if (!stagedDir) {
|
|
304
|
+
stagedDir = fs.mkdtempSync(path.join(os.tmpdir(), `tr-cap-source-${manifest.id}-`));
|
|
305
|
+
isTemp = true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
extractEntriesSafely(entries, stagedDir);
|
|
309
|
+
|
|
310
|
+
const fileDescriptors = entries.map((e) => ({
|
|
311
|
+
path: e.path,
|
|
312
|
+
size: e.size || e.data.length,
|
|
313
|
+
sha256: crypto.createHash('sha256').update(e.data).digest('hex')
|
|
314
|
+
}));
|
|
315
|
+
|
|
316
|
+
const totalBytes = fileDescriptors.reduce((sum, f) => sum + f.size, 0);
|
|
317
|
+
|
|
318
|
+
return {
|
|
319
|
+
id: manifest.id,
|
|
320
|
+
version: manifest.version,
|
|
321
|
+
manifest,
|
|
322
|
+
sourceUri,
|
|
323
|
+
sourceType,
|
|
324
|
+
sha256: computedSha,
|
|
325
|
+
fileCount: entries.length,
|
|
326
|
+
sizeBytes: totalBytes,
|
|
327
|
+
stagedDir,
|
|
328
|
+
files: fileDescriptors,
|
|
329
|
+
cleanup: () => {
|
|
330
|
+
if (isTemp && fs.existsSync(stagedDir)) {
|
|
331
|
+
try {
|
|
332
|
+
fs.rmSync(stagedDir, { recursive: true, force: true });
|
|
333
|
+
} catch {}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Total Recall — Standalone Application Creator
|
|
3
|
+
*
|
|
4
|
+
* Implements Phase 3 of CAPABILITY_DEPLOYMENT_PLUGINS:
|
|
5
|
+
* - Scaffolds clean-room runnable standalone application shell
|
|
6
|
+
* - Initializes SSSS vault and project Total Recall brain
|
|
7
|
+
* - Deploys initial capability plugin in isolated staging
|
|
8
|
+
* - Verifies static gates before atomic publication to target directory
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'node:fs';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import os from 'node:os';
|
|
14
|
+
import { applyDeploymentPlan, resolveAppVaultDir, formatSsssDocument } from './apply.mjs';
|
|
15
|
+
import { createDeploymentPlan } from './plan.mjs';
|
|
16
|
+
|
|
17
|
+
export class AppCreateError extends Error {
|
|
18
|
+
constructor(message, details = {}) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = 'AppCreateError';
|
|
21
|
+
this.details = details;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Copies a directory recursively from src to dest.
|
|
27
|
+
* @param {string} src
|
|
28
|
+
* @param {string} dest
|
|
29
|
+
*/
|
|
30
|
+
function copyDirSync(src, dest) {
|
|
31
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
32
|
+
for (const item of fs.readdirSync(src, { withFileTypes: true })) {
|
|
33
|
+
const srcPath = path.join(src, item.name);
|
|
34
|
+
const destPath = path.join(dest, item.name);
|
|
35
|
+
if (item.isDirectory()) {
|
|
36
|
+
copyDirSync(srcPath, destPath);
|
|
37
|
+
} else {
|
|
38
|
+
fs.copyFileSync(srcPath, destPath);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Scaffolds and publishes a new standalone application with an initial capability plugin.
|
|
45
|
+
*
|
|
46
|
+
* @param {string} targetDir - Destination directory for the new app
|
|
47
|
+
* @param {object} options
|
|
48
|
+
* @param {string} options.plugin - Source path or identifier for initial capability
|
|
49
|
+
* @param {string} [options.adapter] - Target adapter (default: 'ssss-app')
|
|
50
|
+
* @param {string} [options.name] - Application name
|
|
51
|
+
* @param {boolean} [options.force] - Allow overwriting non-empty target directory
|
|
52
|
+
* @param {boolean} [options.initBrain] - Initialize project Total Recall brain (default: true)
|
|
53
|
+
* @param {boolean} [options.runGates] - Run pre-publish verification gates (default: true)
|
|
54
|
+
* @returns {Promise<{
|
|
55
|
+
* success: boolean,
|
|
56
|
+
* targetDir: string,
|
|
57
|
+
* name: string,
|
|
58
|
+
* adapter: string,
|
|
59
|
+
* capabilities: string[],
|
|
60
|
+
* published_at: string
|
|
61
|
+
* }>}
|
|
62
|
+
*/
|
|
63
|
+
export async function createStandaloneApp(targetDir, options = {}) {
|
|
64
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
65
|
+
const appName = options.name || path.basename(resolvedTarget);
|
|
66
|
+
const adapter = options.adapter || 'ssss-app';
|
|
67
|
+
const initBrain = options.initBrain !== false;
|
|
68
|
+
const runGates = options.runGates !== false;
|
|
69
|
+
|
|
70
|
+
if (!options.plugin) {
|
|
71
|
+
throw new AppCreateError('Missing required capability plugin source (--plugin)');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Guard against accidental overwrite
|
|
75
|
+
if (fs.existsSync(resolvedTarget) && !options.force) {
|
|
76
|
+
const items = fs.readdirSync(resolvedTarget);
|
|
77
|
+
if (items.length > 0) {
|
|
78
|
+
throw new AppCreateError(
|
|
79
|
+
`Target directory '${resolvedTarget}' is not empty. Use --force to overwrite.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// 1. Staged creation in isolated directory
|
|
85
|
+
const stagingDir = fs.mkdtempSync(path.join(os.tmpdir(), 'tr-app-create-'));
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// 2. Scaffold standard SSSS application shell
|
|
89
|
+
const packageJson = {
|
|
90
|
+
name: appName,
|
|
91
|
+
version: '0.1.0',
|
|
92
|
+
type: 'module',
|
|
93
|
+
scripts: {
|
|
94
|
+
start: 'node index.mjs',
|
|
95
|
+
check: 'node check.mjs'
|
|
96
|
+
},
|
|
97
|
+
ssss: {
|
|
98
|
+
spec_version: '0.9.6',
|
|
99
|
+
adapter
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
fs.writeFileSync(
|
|
103
|
+
path.join(stagingDir, 'package.json'),
|
|
104
|
+
JSON.stringify(packageJson, null, 2),
|
|
105
|
+
'utf8'
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
// Standalone entrypoint runnable without Total Recall daemon
|
|
109
|
+
const indexJs = `/**
|
|
110
|
+
* ${appName} — Standalone SSSS Application Entrypoint
|
|
111
|
+
* Generated by Total Recall Capability Engine.
|
|
112
|
+
* Operates independently with zero external database dependencies.
|
|
113
|
+
*/
|
|
114
|
+
|
|
115
|
+
import fs from 'node:fs';
|
|
116
|
+
import path from 'node:path';
|
|
117
|
+
|
|
118
|
+
export function getAppInfo() {
|
|
119
|
+
return {
|
|
120
|
+
name: '${appName}',
|
|
121
|
+
adapter: '${adapter}',
|
|
122
|
+
status: 'ready',
|
|
123
|
+
vault: path.resolve('vault')
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (import.meta.url === \`file://\${process.argv[1]}\`) {
|
|
128
|
+
console.log('🚀 ${appName} started in standalone mode');
|
|
129
|
+
console.log('📦 SSSS Adapter: ${adapter}');
|
|
130
|
+
console.log('📁 Vault Root: ', path.resolve('vault'));
|
|
131
|
+
}
|
|
132
|
+
`;
|
|
133
|
+
fs.writeFileSync(path.join(stagingDir, 'index.mjs'), indexJs, 'utf8');
|
|
134
|
+
|
|
135
|
+
// Standalone verification gate runner
|
|
136
|
+
const checkJs = `/**
|
|
137
|
+
* ${appName} — Verification Gate
|
|
138
|
+
*/
|
|
139
|
+
import fs from 'node:fs';
|
|
140
|
+
import path from 'node:path';
|
|
141
|
+
|
|
142
|
+
export function runChecks() {
|
|
143
|
+
const vaultDir = path.resolve('vault');
|
|
144
|
+
if (!fs.existsSync(vaultDir)) {
|
|
145
|
+
throw new Error('Missing required SSSS vault directory');
|
|
146
|
+
}
|
|
147
|
+
return { passed: true, checks: ['vault_present', 'entrypoint_present'] };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (import.meta.url === \`file://\${process.argv[1]}\`) {
|
|
151
|
+
const result = runChecks();
|
|
152
|
+
console.log('✔ All verification gates passed:', result.checks.join(', '));
|
|
153
|
+
}
|
|
154
|
+
`;
|
|
155
|
+
fs.writeFileSync(path.join(stagingDir, 'check.mjs'), checkJs, 'utf8');
|
|
156
|
+
|
|
157
|
+
// SSSS Vault structure
|
|
158
|
+
const vaultDir = path.join(stagingDir, 'vault');
|
|
159
|
+
fs.mkdirSync(path.join(vaultDir, 'system', 'capabilities'), { recursive: true });
|
|
160
|
+
fs.mkdirSync(path.join(vaultDir, 'system', 'grants'), { recursive: true });
|
|
161
|
+
fs.mkdirSync(path.join(vaultDir, 'system', 'events'), { recursive: true });
|
|
162
|
+
|
|
163
|
+
// 3. Initialize project Total Recall brain if requested
|
|
164
|
+
if (initBrain) {
|
|
165
|
+
const brainDir = path.join(stagingDir, '.agent', 'skills', 'total-recall');
|
|
166
|
+
fs.mkdirSync(path.join(brainDir, 'memory-vault'), { recursive: true });
|
|
167
|
+
fs.mkdirSync(path.join(stagingDir, '.agent', 'commands'), { recursive: true });
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 4. Deploy initial capability plugin into staging
|
|
171
|
+
const plan = await createDeploymentPlan(options.plugin, {
|
|
172
|
+
target: stagingDir,
|
|
173
|
+
adapter
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
if (!plan.valid) {
|
|
177
|
+
throw new AppCreateError(
|
|
178
|
+
`Capability plugin is invalid for target app: ${plan.conflicts?.length || 0} conflict(s)`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const applied = await applyDeploymentPlan(plan, {
|
|
183
|
+
target: stagingDir,
|
|
184
|
+
actor: 'total-recall/app-create'
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Write standard README.md if capability did not supply one
|
|
188
|
+
const readmePath = path.join(stagingDir, 'README.md');
|
|
189
|
+
if (!fs.existsSync(readmePath)) {
|
|
190
|
+
fs.writeFileSync(
|
|
191
|
+
readmePath,
|
|
192
|
+
`# ${appName}\n\nStandalone SSSS application powered by the Total Recall capability engine.\n`,
|
|
193
|
+
'utf8'
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// 5. Verification Gates
|
|
198
|
+
if (runGates) {
|
|
199
|
+
if (!fs.existsSync(path.join(stagingDir, 'package.json'))) {
|
|
200
|
+
throw new AppCreateError('Verification gate failed: package.json missing in staged app');
|
|
201
|
+
}
|
|
202
|
+
if (!fs.existsSync(path.join(stagingDir, 'index.mjs'))) {
|
|
203
|
+
throw new AppCreateError('Verification gate failed: entrypoint index.mjs missing');
|
|
204
|
+
}
|
|
205
|
+
for (const capId of applied.capabilities) {
|
|
206
|
+
const capRecord = path.join(stagingDir, 'vault', 'system', 'capabilities', `${capId}.md`);
|
|
207
|
+
if (!fs.existsSync(capRecord)) {
|
|
208
|
+
throw new AppCreateError(
|
|
209
|
+
`Verification gate failed: capability installation record missing for '${capId}'`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 6. Atomic Publication to target directory
|
|
216
|
+
if (fs.existsSync(resolvedTarget) && options.force) {
|
|
217
|
+
fs.rmSync(resolvedTarget, { recursive: true, force: true });
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
fs.mkdirSync(path.dirname(resolvedTarget), { recursive: true });
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
fs.renameSync(stagingDir, resolvedTarget);
|
|
224
|
+
} catch {
|
|
225
|
+
// Fallback if renaming crosses device boundaries
|
|
226
|
+
copyDirSync(stagingDir, resolvedTarget);
|
|
227
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
success: true,
|
|
232
|
+
targetDir: resolvedTarget,
|
|
233
|
+
name: appName,
|
|
234
|
+
adapter,
|
|
235
|
+
capabilities: applied.capabilities,
|
|
236
|
+
published_at: new Date().toISOString()
|
|
237
|
+
};
|
|
238
|
+
} catch (err) {
|
|
239
|
+
if (fs.existsSync(stagingDir)) {
|
|
240
|
+
try {
|
|
241
|
+
fs.rmSync(stagingDir, { recursive: true, force: true });
|
|
242
|
+
} catch {}
|
|
243
|
+
}
|
|
244
|
+
throw new AppCreateError(`Failed to create standalone application: ${err.message}`, {
|
|
245
|
+
error: err
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
}
|