total-recall-brain 3.13.0 → 3.13.2
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/README.md +91 -108
- package/bin/antigravity.mjs +81 -52
- package/bin/total-recall.mjs +32 -57
- package/extension/background.js +1 -1
- package/extension/options/options.js +9 -3
- package/extension/sidepanel/sidepanel.js +0 -2
- package/frontend/index.html +4 -3
- package/frontend/package.json +1 -0
- package/frontend/src/App.tsx +109 -152
- package/frontend/src/api.ts +605 -23
- package/frontend/src/components/BrainSelector.tsx +211 -199
- package/frontend/src/components/DocumentEditorModal.tsx +57 -0
- package/frontend/src/components/DocumentTable.tsx +47 -0
- package/frontend/src/components/Graph3D.tsx +120 -71
- package/frontend/src/components/UsageChart.tsx +3 -3
- package/frontend/src/components/brand/BrandMark.tsx +95 -0
- package/frontend/src/index.css +286 -86
- package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
- package/frontend/src/pages/ChatPage.tsx +103 -45
- package/frontend/src/pages/DesignDocsPage.tsx +5 -5
- package/frontend/src/pages/FilesPage.tsx +10 -6
- package/frontend/src/pages/GraphPage.tsx +5 -3
- package/frontend/src/pages/HelpPage.tsx +2 -2
- package/frontend/src/pages/InboxPage.tsx +82 -0
- package/frontend/src/pages/InstructionsPage.tsx +1 -1
- package/frontend/src/pages/IntegrationsPage.tsx +21 -10
- package/frontend/src/pages/LoginPage.tsx +95 -77
- package/frontend/src/pages/MemoryPage.tsx +31 -8
- package/frontend/src/pages/OnboardingPage.tsx +690 -0
- package/frontend/src/pages/OpenWikiPage.tsx +412 -323
- package/frontend/src/pages/SettingsPage.tsx +825 -922
- package/frontend/src/pages/SkillsPage.tsx +272 -147
- package/frontend/src/pages/UsagePage.tsx +23 -31
- package/frontend/src/pages/VaultPage.tsx +223 -0
- package/frontend/src/types.ts +13 -0
- package/package.json +4 -4
- package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
- package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
- package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
- package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents → modules/agents}/agents.yml +8 -0
- package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
- package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
- package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
- package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
- package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
- package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
- package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
- package/src/cli/brain.mjs +63 -5
- package/src/cli/connect.mjs +11 -19
- package/src/cli/daemon.mjs +5 -4
- package/src/cli/deploy-ui.mjs +8 -3
- package/src/cli/deploy.mjs +84 -27
- package/src/cli/ingest/index.mjs +1 -1
- package/src/cli/init.mjs +63 -34
- package/src/cli/remember.mjs +3 -3
- package/src/cli/secret.mjs +812 -0
- package/src/cli/setup.mjs +34 -7
- package/src/cli/share.mjs +2 -2
- package/src/cli/skill.mjs +396 -35
- package/src/cli/sync.mjs +4 -4
- package/src/cli/task.mjs +287 -0
- package/src/core/clarity-rewriter.mjs +5 -5
- package/src/core/config.mjs +44 -2
- package/src/core/conflict-detector.mjs +5 -5
- package/src/core/crons.mjs +54 -0
- package/src/core/daemon-loop.mjs +87 -235
- package/src/core/dream.mjs +170 -10
- package/src/core/env-import.mjs +456 -0
- package/src/core/fact-seeker.mjs +14 -14
- package/src/core/import-rules.mjs +20 -7
- package/src/core/inference-engine.mjs +3 -3
- package/src/core/migrate.mjs +14 -8
- package/src/core/okf-adapter.mjs +2 -2
- package/src/core/operation-validator.mjs +39 -331
- package/src/core/optimizer.mjs +7 -0
- package/src/core/post-mortem.mjs +2 -2
- package/src/core/project-brain.mjs +349 -0
- package/src/core/provider-catalog.mjs +296 -0
- package/src/core/quick-capture.mjs +1 -1
- package/src/core/remote-vault-sync.mjs +194 -0
- package/src/core/research-queue.mjs +7 -11
- package/src/core/runtime.mjs +33 -9
- package/src/core/scheduler.mjs +59 -5
- package/src/core/schema.mjs +14 -0
- package/src/core/search.mjs +1 -1
- package/src/core/secrets-env-export.mjs +323 -0
- package/src/core/secrets-rotate.mjs +245 -0
- package/src/core/secrets-store.mjs +645 -0
- package/src/core/semantic-index.mjs +3 -3
- package/src/core/skills-registry.mjs +1062 -0
- package/src/core/source-adapters.mjs +4 -1
- package/src/core/ssss-host-extension.mjs +94 -0
- package/src/core/ssss-kernel-bridge.mjs +530 -0
- package/src/core/surface.mjs +14 -0
- package/src/core/task-envelope.mjs +298 -0
- package/src/core/task-executors.mjs +600 -0
- package/src/core/validated-write.mjs +124 -56
- package/src/core/vault.mjs +21 -34
- package/src/core/webauthn-store.mjs +272 -0
- package/src/server/api.mjs +26 -16
- package/src/server/auth.mjs +51 -0
- package/src/server/index.mjs +3 -3
- package/src/server/keys.mjs +30 -4
- package/src/server/rest.mjs +198 -25
- package/src/server/route-manifest.json +156 -0
- package/src/server/routes/_shared.mjs +40 -1
- package/src/server/routes/docs.mjs +260 -0
- package/src/server/routes/memory.mjs +145 -32
- package/src/server/routes/research.mjs +30 -0
- package/src/server/routes/secrets.mjs +470 -0
- package/src/server/routes/share.mjs +2 -2
- package/src/server/routes/skills.mjs +452 -54
- package/src/server/routes/sync.mjs +48 -0
- package/src/server/routes/webauthn.mjs +184 -0
- package/src/server/update-route-manifest.mjs +39 -0
- package/templates/onboarding-interview.md +32 -22
- package/templates/openwiki/README.md +10 -0
- package/templates/openwiki/architecture.md +5 -0
- package/templates/openwiki/ide.md +10 -0
- package/templates/openwiki/memory.md +11 -0
- package/templates/openwiki/secrets.md +7 -0
- package/templates/openwiki/skills.md +7 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
- package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
- package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
- /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals → modules/skill-deploy/scripts}/.gitkeep +0 -0
- /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/find-skills.mjs +0 -0
- /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/install-skill.mjs +0 -0
- /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/scan-skill.mjs +0 -0
package/src/core/migrate.mjs
CHANGED
|
@@ -31,6 +31,9 @@ export async function runMigration(vaultDir, fromVersion, toVersion, migrationFn
|
|
|
31
31
|
to_version: toVersion,
|
|
32
32
|
status: 'pending',
|
|
33
33
|
description,
|
|
34
|
+
title: `Migration v${fromVersion}→v${toVersion}`,
|
|
35
|
+
timestamp: new Date().toISOString(),
|
|
36
|
+
category: 'migrations',
|
|
34
37
|
};
|
|
35
38
|
|
|
36
39
|
try {
|
|
@@ -44,7 +47,7 @@ export async function runMigration(vaultDir, fromVersion, toVersion, migrationFn
|
|
|
44
47
|
// Note: in a real system we'd diff and only write changed, but migration often touches all.
|
|
45
48
|
for (const node of migratedNodes) {
|
|
46
49
|
// In a real migration we might delete old files if slug changes, but this assumes in-place update
|
|
47
|
-
writeNode(node, vaultDir);
|
|
50
|
+
await writeNode(node, vaultDir);
|
|
48
51
|
}
|
|
49
52
|
|
|
50
53
|
migrationRecord.status = 'applied';
|
|
@@ -60,13 +63,16 @@ export async function runMigration(vaultDir, fromVersion, toVersion, migrationFn
|
|
|
60
63
|
logger.error('migration', `WARNING: Vault may be in an inconsistent state. Please run 'total-recall snapshot rollback ${snapshotId}' immediately to recover.`);
|
|
61
64
|
throw err;
|
|
62
65
|
} finally {
|
|
63
|
-
// 3. Record the migration event
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
66
|
+
// 3. Record the migration event via Operation Contract (migrations/<slug>.md)
|
|
67
|
+
const slug = `migration-v${toVersion}-${migrationId}`.replace(/[^a-zA-Z0-9_-]/g, '-');
|
|
68
|
+
await writeNode({
|
|
69
|
+
...migrationRecord,
|
|
70
|
+
slug,
|
|
71
|
+
category: 'migrations',
|
|
72
|
+
title: migrationRecord.title || slug,
|
|
73
|
+
description: description || slug,
|
|
74
|
+
timestamp: migrationRecord.applied_at || new Date().toISOString(),
|
|
75
|
+
}, vaultDir);
|
|
70
76
|
}
|
|
71
77
|
|
|
72
78
|
return { migrationId, status: migrationRecord.status };
|
package/src/core/okf-adapter.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import crypto from 'crypto';
|
|
4
4
|
import matter from 'gray-matter';
|
|
5
5
|
import { safeStringify } from './vault.mjs';
|
|
6
|
-
import {
|
|
6
|
+
import { writeNodeValidatedAsync } from './validated-write.mjs';
|
|
7
7
|
import { getNodes } from './vault-cache.mjs';
|
|
8
8
|
import { validateMemoryNode } from './total-recall-memory-validator.mjs';
|
|
9
9
|
|
|
@@ -280,7 +280,7 @@ export async function importBundle(bundlePath, vaultDir, options = {}) {
|
|
|
280
280
|
}
|
|
281
281
|
|
|
282
282
|
try {
|
|
283
|
-
const writeResult =
|
|
283
|
+
const writeResult = await writeNodeValidatedAsync(node, vaultDir, {
|
|
284
284
|
dryRun: options.dryRun || false,
|
|
285
285
|
agentRole: options.agentRole || 'admin'
|
|
286
286
|
});
|
|
@@ -1,355 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Total Recall Operation Contract entry points.
|
|
3
|
+
*
|
|
4
|
+
* All mutations go through the SSSS 0.9 package kernel via
|
|
5
|
+
* `processViaPackageKernel`. The local Stage 1–7 pipeline has been removed.
|
|
6
|
+
*/
|
|
1
7
|
import fs from 'fs';
|
|
2
8
|
import path from 'path';
|
|
3
|
-
import matter from 'gray-matter';
|
|
4
9
|
import crypto from 'node:crypto';
|
|
5
|
-
import {
|
|
6
|
-
SSSS_SCHEMAS,
|
|
7
|
-
OperationEnvelopeSchema,
|
|
8
|
-
PatchEnvelopeSchema,
|
|
9
|
-
EventEnvelopeSchema,
|
|
10
|
-
DeleteEnvelopeSchema,
|
|
11
|
-
} from './schema.mjs';
|
|
12
|
-
import { atomicWrite, safeStringify } from './vault.mjs';
|
|
13
|
-
import { logger } from './logger.mjs';
|
|
14
|
-
import { validateMemoryNode } from './total-recall-memory-validator.mjs';
|
|
15
|
-
|
|
16
|
-
const APPEND_TYPES = new Set(['conversation', 'run']);
|
|
17
|
-
const PROTOCOL_PATHS = [
|
|
18
|
-
'references/ssss-spec.md',
|
|
19
|
-
'references/admin-protocol-evolution-policy.md',
|
|
20
|
-
'references/user-local-optimizer-boundary-policy.md',
|
|
21
|
-
];
|
|
22
|
-
const PROTOCOL_PATH_PREFIXES = ['fixtures/'];
|
|
23
|
-
const idempotencyCache = new Map();
|
|
24
|
-
const IDEMPOTENCY_TTL_MS = 10 * 60 * 1000;
|
|
25
|
-
const warmedVaults = new Set();
|
|
26
|
-
|
|
27
|
-
function cacheKey(wid, key) { return `${wid}:${key}`; }
|
|
28
|
-
|
|
29
|
-
function pruneExpired() {
|
|
30
|
-
const now = Date.now();
|
|
31
|
-
for (const [k, v] of idempotencyCache) {
|
|
32
|
-
if (now - v.ts > IDEMPOTENCY_TTL_MS) idempotencyCache.delete(k);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function warmIdempotencyCache(vaultRoot, eventLogDir) {
|
|
37
|
-
const vaultKey = path.resolve(vaultRoot);
|
|
38
|
-
if (warmedVaults.has(vaultKey)) return;
|
|
39
|
-
warmedVaults.add(vaultKey);
|
|
40
|
-
|
|
41
|
-
const auditDir = eventLogDir || path.join(vaultRoot, '.events');
|
|
42
|
-
const auditFile = path.join(auditDir, 'audit.jsonl');
|
|
43
|
-
if (!fs.existsSync(auditFile)) return;
|
|
44
|
-
|
|
45
|
-
try {
|
|
46
|
-
const lines = fs.readFileSync(auditFile, 'utf8').split('\n');
|
|
47
|
-
const now = Date.now();
|
|
48
|
-
for (const line of lines) {
|
|
49
|
-
if (!line.trim()) continue;
|
|
50
|
-
try {
|
|
51
|
-
const record = JSON.parse(line);
|
|
52
|
-
if (record.event_type !== 'audit' || !record.payload) continue;
|
|
53
|
-
const ts = Date.parse(record.ts);
|
|
54
|
-
if (isNaN(ts) || now - ts > IDEMPOTENCY_TTL_MS) continue;
|
|
55
|
-
|
|
56
|
-
const ck = cacheKey(record.payload.workspace_id, record.payload.idempotency_key);
|
|
57
|
-
const response = {
|
|
58
|
-
success: true,
|
|
59
|
-
type: record.payload.envelope_type,
|
|
60
|
-
operation_id: record.correlation_id,
|
|
61
|
-
path: record.subject,
|
|
62
|
-
committed_at: record.ts,
|
|
63
|
-
validation: {
|
|
64
|
-
valid: true,
|
|
65
|
-
type: record.payload.resolved_type,
|
|
66
|
-
errors: [],
|
|
67
|
-
warnings: []
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
idempotencyCache.set(ck, { response, ts });
|
|
71
|
-
} catch { /* skip corrupt lines */ }
|
|
72
|
-
}
|
|
73
|
-
} catch (err) {
|
|
74
|
-
logger.error('operation-validator', `Failed to warm idempotency cache: ${err.message}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function isProtocolPath(fp) {
|
|
79
|
-
for (const p of PROTOCOL_PATHS) if (fp === p || fp.endsWith(`/${p}`)) return true;
|
|
80
|
-
for (const prefix of PROTOCOL_PATH_PREFIXES) if (fp.startsWith(prefix) || fp.includes(`/${prefix}`)) return true;
|
|
81
|
-
if (fp.endsWith('schema.mjs') || fp.endsWith('schema.spec.mjs')) return true;
|
|
82
|
-
return false;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
function buildRepair(zodError) {
|
|
86
|
-
return { field_errors: (zodError.issues || []).map(i => ({ field: i.path.join('.') || '(root)', issue: i.message })) };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function resolveVfsPath(vaultRoot, vfsPath) {
|
|
90
|
-
const cleaned = vfsPath.replace(/^\/+/, '');
|
|
91
|
-
const resolved = path.resolve(vaultRoot, cleaned);
|
|
92
|
-
if (!resolved.startsWith(path.resolve(vaultRoot))) throw new Error(`Path traversal: ${vfsPath}`);
|
|
93
|
-
return resolved;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function makeErrorResponse(type, opId, p, errors, resolvedType = undefined, repairEntries = undefined) {
|
|
97
|
-
const response = {
|
|
98
|
-
success: false,
|
|
99
|
-
type,
|
|
100
|
-
operation_id: opId,
|
|
101
|
-
path: p,
|
|
102
|
-
committed_at: null,
|
|
103
|
-
validation: { valid: false, type: resolvedType, errors, warnings: [] }
|
|
104
|
-
};
|
|
105
|
-
if (repairEntries) response.repair = { field_errors: repairEntries };
|
|
106
|
-
return response;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function getActorRole(envelope, options) {
|
|
110
|
-
return options.agentRole || envelope.actor?.role || null;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function loadRolePermissions(vaultRoot, role) {
|
|
114
|
-
if (role === 'system') return ['*:*'];
|
|
115
|
-
if (role === 'admin') return ['write:*', 'read:*'];
|
|
116
|
-
if (role === 'optimizer') return ['write:memory'];
|
|
117
|
-
if (!role) return [];
|
|
118
|
-
|
|
119
|
-
const roleFile = path.join(vaultRoot, 'roles', role, 'ROLE.md');
|
|
120
|
-
if (!fs.existsSync(roleFile)) return [];
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
const { data } = matter(fs.readFileSync(roleFile, 'utf8'));
|
|
124
|
-
return Array.isArray(data.permissions) ? data.permissions : [];
|
|
125
|
-
} catch {
|
|
126
|
-
return [];
|
|
127
|
-
}
|
|
128
|
-
}
|
|
10
|
+
import { processViaPackageKernel } from './ssss-kernel-bridge.mjs';
|
|
129
11
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
12
|
+
/**
|
|
13
|
+
* @deprecated Sync API removed. Use {@link processOperationAsync}.
|
|
14
|
+
*/
|
|
15
|
+
export function processOperation(_envelope, _vaultRoot, _options = {}) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
'processOperation (sync) was removed. Use processOperationAsync — all mutations go through the SSSS 0.9 package kernel.',
|
|
18
|
+
);
|
|
136
19
|
}
|
|
137
20
|
|
|
138
21
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* @param {string} vaultRoot - Absolute path to the vault directory.
|
|
142
|
-
* @param {object} [options] - { agentRole, leaseStore, eventLogDir }
|
|
143
|
-
* @returns {object} OperationResponse (§6.4).
|
|
22
|
+
* Primary Operation Contract entry for Total Recall.
|
|
23
|
+
* Always routes through `@ssss/cli` package kernel + TR host adapters.
|
|
144
24
|
*/
|
|
145
|
-
export function
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
// Stage 1: Envelope Validation
|
|
152
|
-
const schemaMap = { operation: OperationEnvelopeSchema, patch: PatchEnvelopeSchema, event: EventEnvelopeSchema, delete: DeleteEnvelopeSchema };
|
|
153
|
-
const envSchema = schemaMap[envelope.type];
|
|
154
|
-
if (!envSchema) return makeErrorResponse(envelope.type || 'unknown', operationId, envelope.path || '', [`Unknown envelope type: ${envelope.type}`]);
|
|
155
|
-
const envResult = envSchema.safeParse(envelope);
|
|
156
|
-
if (!envResult.success) return { ...makeErrorResponse(envelope.type, operationId, envelope.path || '', envResult.error.issues.map(i => `${i.path.join('.')}: ${i.message}`)), repair: buildRepair(envResult.error) };
|
|
157
|
-
|
|
158
|
-
// Warm idempotency cache from audit logs if not already done
|
|
159
|
-
warmIdempotencyCache(vaultRoot, eventLogDir);
|
|
160
|
-
|
|
161
|
-
// Stage 2: Idempotency
|
|
162
|
-
pruneExpired();
|
|
163
|
-
const ck = cacheKey(envelope.workspace_id, envelope.idempotency_key);
|
|
164
|
-
const cached = idempotencyCache.get(ck);
|
|
165
|
-
if (cached) return { ...cached.response, replay: cached.response };
|
|
166
|
-
|
|
167
|
-
// Stage 3: Authorization
|
|
168
|
-
if (isProtocolPath(envelope.path) && actorRole !== 'admin' && actorRole !== 'system') return makeErrorResponse(envelope.type, operationId, envelope.path, [`Protocol path '${envelope.path}' requires admin role.`]);
|
|
169
|
-
|
|
170
|
-
// Stage 4: Lease Check
|
|
171
|
-
if (leaseStore) {
|
|
172
|
-
const lc = checkLease(envelope, leaseStore);
|
|
173
|
-
if (!lc.ok) return makeErrorResponse(envelope.type, operationId, envelope.path, [lc.error]);
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Stage 5: Content Validation
|
|
177
|
-
let resolvedType = null;
|
|
178
|
-
const errors = [];
|
|
179
|
-
|
|
180
|
-
if (envelope.type === 'operation' || envelope.type === 'patch') {
|
|
181
|
-
try {
|
|
182
|
-
let fmData;
|
|
183
|
-
if (envelope.type === 'operation') {
|
|
184
|
-
fmData = matter(envelope.content).data;
|
|
185
|
-
} else {
|
|
186
|
-
const absPath = resolveVfsPath(vaultRoot, envelope.path);
|
|
187
|
-
if (!fs.existsSync(absPath)) { errors.push(`Target file does not exist for patch: ${envelope.path}`); }
|
|
188
|
-
else {
|
|
189
|
-
const { data, content: body } = matter(fs.readFileSync(absPath, 'utf8'));
|
|
190
|
-
fmData = { ...data, ...envelope.patches };
|
|
191
|
-
if (envelope.patches.__body__ !== undefined && APPEND_TYPES.has(data.type)) {
|
|
192
|
-
const eb = body.trim(), nb = String(envelope.patches.__body__).trim();
|
|
193
|
-
if (eb && !nb.startsWith(eb)) errors.push(`Append-type '${data.type}' does not allow rewriting existing records.`);
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
if (fmData && errors.length === 0) {
|
|
198
|
-
resolvedType = fmData.type;
|
|
199
|
-
if (!resolvedType) errors.push('Missing required frontmatter field: type');
|
|
200
|
-
else if (!SSSS_SCHEMAS[resolvedType]) errors.push(`Unknown SSSS type: '${resolvedType}'.`);
|
|
201
|
-
else {
|
|
202
|
-
if (resolvedType === 'memory' && envelope.path.includes('.agent/memory-vault/')) {
|
|
203
|
-
const vResult = validateMemoryNode(fmData);
|
|
204
|
-
if (!vResult.success) vResult.errors.forEach(e => errors.push(e));
|
|
205
|
-
} else {
|
|
206
|
-
const r = SSSS_SCHEMAS[resolvedType].safeParse(fmData);
|
|
207
|
-
if (!r.success) r.error.issues.forEach(i => errors.push(`${i.path.join('.')}: ${i.message}`));
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
if (envelope.type === 'operation' && APPEND_TYPES.has(resolvedType)) {
|
|
211
|
-
const ap = resolveVfsPath(vaultRoot, envelope.path);
|
|
212
|
-
if (fs.existsSync(ap)) {
|
|
213
|
-
const eb = matter(fs.readFileSync(ap, 'utf8')).content.trim();
|
|
214
|
-
const nb = matter(envelope.content).content.trim();
|
|
215
|
-
if (eb && !nb.startsWith(eb)) errors.push(`Append-type '${resolvedType}' does not allow rewriting existing records.`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
if (actorRole === 'optimizer' && resolvedType === 'memory') {
|
|
219
|
-
if (fmData.priority === 'absolute') warnings.push('Optimizer writing Tier 1 (priority: absolute) node.');
|
|
220
|
-
if (fmData.immutable === true) warnings.push('Optimizer writing immutable node.');
|
|
221
|
-
}
|
|
222
|
-
if (resolvedType === 'schema-proposal' && fmData.status === 'accepted') {
|
|
223
|
-
if (actorRole !== 'admin') errors.push('Only admin may accept schema-proposals.');
|
|
224
|
-
if (!fmData.reviewed_by) errors.push('Accepted schema-proposals must have reviewed_by.');
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
} catch (err) { errors.push(`Content parsing error: ${err.message}`); }
|
|
229
|
-
}
|
|
230
|
-
if (envelope.type === 'event') { try { JSON.parse(envelope.content); } catch { errors.push('Event content must be valid JSON.'); } }
|
|
231
|
-
|
|
232
|
-
if (envelope.type === 'delete') {
|
|
233
|
-
const absPath = resolveVfsPath(vaultRoot, envelope.path);
|
|
234
|
-
if (!fs.existsSync(absPath)) { errors.push(`Target file does not exist for delete: ${envelope.path}`); }
|
|
235
|
-
else {
|
|
236
|
-
const { data } = matter(fs.readFileSync(absPath, 'utf8'));
|
|
237
|
-
resolvedType = data.type || null;
|
|
238
|
-
// §6.2: append-type documents are immutable and may not be deleted.
|
|
239
|
-
if (APPEND_TYPES.has(resolvedType)) errors.push(`Append-type '${resolvedType}' may not be deleted.`);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
if (errors.length === 0) {
|
|
244
|
-
const permType = envelope.type === 'event' ? 'event' : resolvedType;
|
|
245
|
-
const requiredPerm = `write:${permType}`;
|
|
246
|
-
const permissions = loadRolePermissions(vaultRoot, actorRole);
|
|
247
|
-
if (!hasPermission(permissions, requiredPerm)) {
|
|
248
|
-
errors.push(`Access denied: role '${actorRole || '(none)'}' lacks permission '${requiredPerm}'.`);
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const isValid = errors.length === 0;
|
|
253
|
-
if (envelope.dry_run || !isValid) {
|
|
254
|
-
const resp = { success: isValid, type: envelope.type, operation_id: operationId, path: envelope.path, committed_at: null, dry_run: !!envelope.dry_run, validation: { valid: isValid, type: resolvedType, errors, warnings } };
|
|
255
|
-
if (!isValid) resp.repair = {
|
|
256
|
-
field_errors: errors.map(e => {
|
|
257
|
-
if (e.startsWith('Access denied:')) {
|
|
258
|
-
return {
|
|
259
|
-
field: 'actor.role',
|
|
260
|
-
issue: actorRole ? 'Insufficient permissions.' : 'Missing actor.role — the envelope was not authorized by a verified identity.'
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
return { field: e.split(': ')[0] || '(unknown)', issue: e };
|
|
264
|
-
})
|
|
265
|
-
};
|
|
266
|
-
return resp;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
// Stage 6: Commit
|
|
270
|
-
const committedAt = new Date().toISOString();
|
|
271
|
-
try {
|
|
272
|
-
const absPath = resolveVfsPath(vaultRoot, envelope.path);
|
|
273
|
-
const dir = path.dirname(absPath);
|
|
274
|
-
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
275
|
-
if (envelope.type === 'operation') {
|
|
276
|
-
let contentToWrite = envelope.content;
|
|
277
|
-
try {
|
|
278
|
-
const parsed = matter(contentToWrite);
|
|
279
|
-
if (parsed.data && parsed.data.type === 'memory') {
|
|
280
|
-
parsed.data.updated = committedAt;
|
|
281
|
-
parsed.data.last_accessed = committedAt;
|
|
282
|
-
contentToWrite = safeStringify(parsed.content, parsed.data);
|
|
283
|
-
}
|
|
284
|
-
} catch {
|
|
285
|
-
// Fallback to raw content if parsing fails
|
|
286
|
-
}
|
|
287
|
-
atomicWrite(absPath, contentToWrite);
|
|
288
|
-
}
|
|
289
|
-
else if (envelope.type === 'patch') {
|
|
290
|
-
const { data, content: body } = matter(fs.readFileSync(absPath, 'utf8'));
|
|
291
|
-
const merged = { ...data };
|
|
292
|
-
for (const [k, v] of Object.entries(envelope.patches)) { if (k !== '__body__') merged[k] = v; }
|
|
293
|
-
if (merged.type === 'memory') {
|
|
294
|
-
merged.updated = committedAt;
|
|
295
|
-
merged.last_accessed = committedAt;
|
|
296
|
-
}
|
|
297
|
-
const newBody = envelope.patches.__body__ !== undefined ? (APPEND_TYPES.has(data.type) ? body + '\n' + envelope.patches.__body__ : envelope.patches.__body__) : body;
|
|
298
|
-
atomicWrite(absPath, safeStringify(newBody, merged));
|
|
299
|
-
} else if (envelope.type === 'event') {
|
|
300
|
-
const logDir = eventLogDir || path.join(vaultRoot, '.events');
|
|
301
|
-
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
|
302
|
-
fs.appendFileSync(path.join(logDir, `${envelope.workspace_id}.jsonl`), JSON.stringify({ event_id: operationId, event_type: 'operation-audit', ts: committedAt, path: envelope.path, content: envelope.content, idempotency_key: envelope.idempotency_key }) + '\n');
|
|
303
|
-
} else if (envelope.type === 'delete') {
|
|
304
|
-
fs.rmSync(absPath, { force: true });
|
|
305
|
-
// §6.2: a delete emits an auditable deletion event so history is never lost.
|
|
306
|
-
const logDir = eventLogDir || path.join(vaultRoot, '.events');
|
|
307
|
-
if (!fs.existsSync(logDir)) fs.mkdirSync(logDir, { recursive: true });
|
|
308
|
-
fs.appendFileSync(path.join(logDir, `${envelope.workspace_id}.jsonl`), JSON.stringify({ event_id: operationId, event_type: 'delete', ts: committedAt, path: envelope.path, resolved_type: resolvedType, idempotency_key: envelope.idempotency_key }) + '\n');
|
|
309
|
-
}
|
|
310
|
-
} catch (err) {
|
|
311
|
-
logger.error('operation-validator', `Commit failed: ${err.message}`);
|
|
312
|
-
return makeErrorResponse(envelope.type, operationId, envelope.path, [`Commit failed: ${err.message}`]);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Stage 7: Audit
|
|
316
|
-
try {
|
|
317
|
-
const auditDir = eventLogDir || path.join(vaultRoot, '.events');
|
|
318
|
-
if (!fs.existsSync(auditDir)) fs.mkdirSync(auditDir, { recursive: true });
|
|
319
|
-
fs.appendFileSync(path.join(auditDir, 'audit.jsonl'), JSON.stringify({ event_id: crypto.randomUUID(), event_type: 'audit', correlation_id: operationId, ts: committedAt, subject: envelope.path, payload: { envelope_type: envelope.type, idempotency_key: envelope.idempotency_key, workspace_id: envelope.workspace_id, agent_role: actorRole, resolved_type: resolvedType } }) + '\n');
|
|
320
|
-
} catch (err) { logger.error('operation-validator', `Audit failed: ${err.message}`); }
|
|
321
|
-
|
|
322
|
-
const response = { success: true, type: envelope.type, operation_id: operationId, path: envelope.path, committed_at: committedAt, validation: { valid: true, type: resolvedType, errors: [], warnings } };
|
|
323
|
-
idempotencyCache.set(ck, { response, ts: Date.now() });
|
|
324
|
-
return response;
|
|
25
|
+
export async function processOperationAsync(envelope, vaultRoot, options = {}) {
|
|
26
|
+
return processViaPackageKernel(envelope, vaultRoot, {
|
|
27
|
+
...options,
|
|
28
|
+
agentRole: options.agentRole || envelope?.actor?.role || 'admin',
|
|
29
|
+
});
|
|
325
30
|
}
|
|
326
31
|
|
|
327
|
-
|
|
328
|
-
const lp = path.join(leaseStore, envelope.workspace_id, `${envelope.path.replace(/\//g, '__')}.lease.json`);
|
|
329
|
-
if (!fs.existsSync(lp)) return { ok: true };
|
|
330
|
-
try {
|
|
331
|
-
const lease = JSON.parse(fs.readFileSync(lp, 'utf8'));
|
|
332
|
-
if (new Date(lease.expires_at) < new Date()) { fs.unlinkSync(lp); return { ok: true }; }
|
|
333
|
-
if (!envelope.lease_id) return { ok: false, error: `Path '${envelope.path}' is leased (${lease.lease_id}). Supply lease_id.` };
|
|
334
|
-
if (envelope.lease_id !== lease.lease_id) return { ok: false, error: `Lease mismatch: got '${envelope.lease_id}', expected '${lease.lease_id}'.` };
|
|
335
|
-
return { ok: true };
|
|
336
|
-
} catch { try { fs.unlinkSync(lp); } catch { } return { ok: true }; }
|
|
337
|
-
}
|
|
32
|
+
// ─── Lease helpers (host-local filesystem lease store, used by tests/API) ────
|
|
338
33
|
|
|
339
34
|
export function acquireLease(workspaceId, vfsPath, leaseStore, ttlMs = 30000) {
|
|
340
35
|
const dir = path.join(leaseStore, workspaceId);
|
|
341
36
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
342
37
|
const lp = path.join(dir, `${vfsPath.replace(/\//g, '__')}.lease.json`);
|
|
343
|
-
if (fs.existsSync(lp)) {
|
|
38
|
+
if (fs.existsSync(lp)) {
|
|
39
|
+
try {
|
|
40
|
+
const e = JSON.parse(fs.readFileSync(lp, 'utf8'));
|
|
41
|
+
if (new Date(e.expires_at) >= new Date()) return { error: `Path '${vfsPath}' already leased.` };
|
|
42
|
+
} catch { /* replace corrupt lease */ }
|
|
43
|
+
}
|
|
344
44
|
const leaseId = crypto.randomUUID();
|
|
345
45
|
const expiresAt = new Date(Date.now() + ttlMs).toISOString();
|
|
346
|
-
fs.writeFileSync(lp, JSON.stringify({
|
|
46
|
+
fs.writeFileSync(lp, JSON.stringify({
|
|
47
|
+
lease_id: leaseId, path: vfsPath, workspace_id: workspaceId, expires_at: expiresAt,
|
|
48
|
+
}, null, 2));
|
|
347
49
|
return { lease_id: leaseId, expires_at: expiresAt };
|
|
348
50
|
}
|
|
349
51
|
|
|
350
52
|
export function releaseLease(workspaceId, vfsPath, leaseId, leaseStore) {
|
|
351
53
|
const lp = path.join(leaseStore, workspaceId, `${vfsPath.replace(/\//g, '__')}.lease.json`);
|
|
352
54
|
if (!fs.existsSync(lp)) return { released: true };
|
|
353
|
-
try {
|
|
354
|
-
|
|
55
|
+
try {
|
|
56
|
+
const e = JSON.parse(fs.readFileSync(lp, 'utf8'));
|
|
57
|
+
if (e.lease_id !== leaseId) return { error: 'Lease ID mismatch.' };
|
|
58
|
+
fs.unlinkSync(lp);
|
|
59
|
+
return { released: true };
|
|
60
|
+
} catch (err) {
|
|
61
|
+
return { error: err.message };
|
|
62
|
+
}
|
|
355
63
|
}
|
package/src/core/optimizer.mjs
CHANGED
|
@@ -18,6 +18,13 @@ export function createProposal(category, summary, targetPath = null, rationale =
|
|
|
18
18
|
return {
|
|
19
19
|
type: 'proposal',
|
|
20
20
|
proposal_id: proposalId,
|
|
21
|
+
slug: proposalId,
|
|
22
|
+
// Vault folder for proposal documents (distinct from proposal.category topic)
|
|
23
|
+
// prepareNodeForContract falls back to category=proposals when absent; set explicitly.
|
|
24
|
+
// Note: SSSS field `category` here is the proposal topic (memory-cleanup, etc.).
|
|
25
|
+
title: summary || proposalId,
|
|
26
|
+
description: rationale || summary || 'Optimizer proposal',
|
|
27
|
+
timestamp: new Date().toISOString(),
|
|
21
28
|
category,
|
|
22
29
|
status: 'draft',
|
|
23
30
|
target_path: targetPath,
|
package/src/core/post-mortem.mjs
CHANGED
|
@@ -219,7 +219,7 @@ export async function runPostMortem(sessionPath, { vaultDir, inboxDir, runtimeCo
|
|
|
219
219
|
// ─── Draft Node Writers ─────────────────────────────────────────────────────────
|
|
220
220
|
|
|
221
221
|
function writeDraftNode(inboxDir, { title, body, category, layer, tags, sourceSession }) {
|
|
222
|
-
const slug = `pm-${category}-${crypto.
|
|
222
|
+
const slug = `pm-${category}-${crypto.createHash('md5').update(sourceSession + title).digest('hex').slice(0, 8)}`;
|
|
223
223
|
const now = new Date().toISOString();
|
|
224
224
|
|
|
225
225
|
const frontmatter = {
|
|
@@ -270,7 +270,7 @@ function writeDraftNode(inboxDir, { title, body, category, layer, tags, sourceSe
|
|
|
270
270
|
}
|
|
271
271
|
|
|
272
272
|
function writeSkillGapTask(queueDir, { topic, description, sourceSession }) {
|
|
273
|
-
const slug = `skill-gap-${
|
|
273
|
+
const slug = `skill-gap-${crypto.createHash('md5').update(topic).digest('hex').slice(0, 6)}`;
|
|
274
274
|
const data = {
|
|
275
275
|
type: 'task',
|
|
276
276
|
slug,
|