@a5c-ai/krate 5.0.1-staging.3a341c33c
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/Dockerfile +31 -0
- package/README.md +183 -0
- package/bin/krate-demo.mjs +23 -0
- package/bin/krate-server.mjs +14 -0
- package/dist/krate-controller-ui.json +2455 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +2955 -0
- package/dist/krate-summary.json +722 -0
- package/docs/README.md +61 -0
- package/docs/agents/README.md +83 -0
- package/docs/agents/acceptance-test-matrix.md +193 -0
- package/docs/agents/agent-mux-adapter-contract.md +167 -0
- package/docs/agents/agent-mux-source-map.md +310 -0
- package/docs/agents/agent-run-memory-import-spec.md +256 -0
- package/docs/agents/agent-stack-management-spec.md +421 -0
- package/docs/agents/api-contract-spec.md +309 -0
- package/docs/agents/artifacts-writeback-spec.md +145 -0
- package/docs/agents/chart-packaging-spec.md +128 -0
- package/docs/agents/ci-orchestration-spec.md +140 -0
- package/docs/agents/context-assembly-spec.md +219 -0
- package/docs/agents/controller-reconciliation-spec.md +255 -0
- package/docs/agents/crd-schema-spec.md +315 -0
- package/docs/agents/decision-log-open-questions.md +169 -0
- package/docs/agents/developer-implementation-checklist.md +329 -0
- package/docs/agents/dispatching-design.md +262 -0
- package/docs/agents/glossary.md +66 -0
- package/docs/agents/implementation-blueprint.md +324 -0
- package/docs/agents/implementation-rollout-slices.md +251 -0
- package/docs/agents/memory-context-integration-spec.md +194 -0
- package/docs/agents/memory-ontology-schema-spec.md +253 -0
- package/docs/agents/memory-operations-runbook.md +121 -0
- package/docs/agents/mvp-vertical-slice-spec.md +146 -0
- package/docs/agents/observability-audit-spec.md +265 -0
- package/docs/agents/operator-runbook.md +174 -0
- package/docs/agents/org-memory-api-payload-examples.md +333 -0
- package/docs/agents/org-memory-controller-sequence-spec.md +181 -0
- package/docs/agents/org-memory-e2e-fixture-plan.md +161 -0
- package/docs/agents/org-memory-ui-implementation-map.md +114 -0
- package/docs/agents/org-memory-vertical-slice-spec.md +168 -0
- package/docs/agents/org-resource-model-delta-spec.md +111 -0
- package/docs/agents/org-route-resource-model-spec.md +183 -0
- package/docs/agents/org-scoping-namespace-spec.md +114 -0
- package/docs/agents/rbac-secrets-management-spec.md +406 -0
- package/docs/agents/repository-page-integration-spec.md +255 -0
- package/docs/agents/resource-contract-examples.md +808 -0
- package/docs/agents/resource-relationship-map.md +190 -0
- package/docs/agents/security-threat-model.md +188 -0
- package/docs/agents/shared-memory-company-brain-spec.md +358 -0
- package/docs/agents/storage-migration-spec.md +168 -0
- package/docs/agents/subagent-orchestration-spec.md +152 -0
- package/docs/agents/system-overview.md +88 -0
- package/docs/agents/tools-mcp-skills-spec.md +189 -0
- package/docs/agents/traceability-matrix.md +79 -0
- package/docs/agents/ui-flow-spec.md +211 -0
- package/docs/agents/ui-ux-system-spec.md +426 -0
- package/docs/agents/workspace-lifecycle-spec.md +166 -0
- package/docs/architecture-spec.md +78 -0
- package/docs/components/control-plane.md +78 -0
- package/docs/components/data-plane.md +69 -0
- package/docs/components/hooks-events.md +67 -0
- package/docs/components/identity-rbac-policy.md +73 -0
- package/docs/components/kubevela-oam.md +70 -0
- package/docs/components/operations-publishing.md +81 -0
- package/docs/components/runners-ci.md +66 -0
- package/docs/components/web-ui.md +94 -0
- package/docs/external/README.md +47 -0
- package/docs/external/bidirectional-sync-design.md +134 -0
- package/docs/external/cicd-interface.md +64 -0
- package/docs/external/external-backend-controllers.md +170 -0
- package/docs/external/external-backend-crds.md +234 -0
- package/docs/external/external-backend-ui-spec.md +151 -0
- package/docs/external/external-backend-ux-flows.md +115 -0
- package/docs/external/external-object-mapping.md +125 -0
- package/docs/external/git-forge-interface.md +68 -0
- package/docs/external/github-integration-design.md +151 -0
- package/docs/external/issue-tracking-interface.md +66 -0
- package/docs/external/provider-capability-manifests.md +204 -0
- package/docs/external/provider-catalog.md +139 -0
- package/docs/external/provider-rollout-testing.md +78 -0
- package/docs/external/research-results.md +48 -0
- package/docs/external/security-auth-permissions.md +81 -0
- package/docs/external/sync-state-machines.md +108 -0
- package/docs/external/unified-external-backend-model.md +107 -0
- package/docs/external/user-facing-changes.md +67 -0
- package/docs/gaps.md +161 -0
- package/docs/install.md +94 -0
- package/docs/krate-design.md +334 -0
- package/docs/local-minikube.md +55 -0
- package/docs/ontology/README.md +32 -0
- package/docs/ontology/bounded-contexts.md +29 -0
- package/docs/ontology/events-and-hooks.md +32 -0
- package/docs/ontology/oam-kubevela.md +32 -0
- package/docs/ontology/operations-and-release.md +25 -0
- package/docs/ontology/personas-and-actors.md +32 -0
- package/docs/ontology/policies-and-invariants.md +33 -0
- package/docs/ontology/problem-space.md +30 -0
- package/docs/ontology/resource-contracts.md +40 -0
- package/docs/ontology/resource-taxonomy.md +42 -0
- package/docs/ontology/runners-and-ci.md +29 -0
- package/docs/ontology/solution-space.md +24 -0
- package/docs/ontology/storage-and-data-boundaries.md +29 -0
- package/docs/ontology/validation-matrix.md +24 -0
- package/docs/ontology/web-ui-excellent-flows.md +32 -0
- package/docs/ontology/workflows.md +39 -0
- package/docs/ontology/world.md +35 -0
- package/docs/product-requirements.md +62 -0
- package/docs/roadmap-mvp.md +87 -0
- package/docs/system-requirements.md +90 -0
- package/docs/tests/README.md +53 -0
- package/docs/tests/agent-qa-plan.md +63 -0
- package/docs/tests/browser-ui-tests.md +62 -0
- package/docs/tests/ci-quality-gates.md +48 -0
- package/docs/tests/coverage-model.md +64 -0
- package/docs/tests/e2e-scenario-tests.md +53 -0
- package/docs/tests/fixtures-test-data.md +63 -0
- package/docs/tests/observability-reliability-tests.md +54 -0
- package/docs/tests/product-test-matrix.md +145 -0
- package/docs/tests/qa-adoption-roadmap.md +130 -0
- package/docs/tests/qa-automation-plan.md +101 -0
- package/docs/tests/security-compliance-tests.md +57 -0
- package/docs/tests/test-framework-tools.md +88 -0
- package/docs/tests/test-suite-layout.md +121 -0
- package/docs/tests/unit-integration-tests.md +48 -0
- package/docs/todo-kyverno +714 -0
- package/docs/user-stories.md +78 -0
- package/examples/minikube-demo.yaml +190 -0
- package/examples/oam-application.yaml +23 -0
- package/examples/policy-kyverno-pr-title.yaml +18 -0
- package/package.json +63 -0
- package/scripts/build.mjs +29 -0
- package/scripts/setup-minikube.mjs +65 -0
- package/scripts/smoke.mjs +37 -0
- package/scripts/validate-doc-coverage.mjs +152 -0
- package/scripts/validate-package.mjs +93 -0
- package/scripts/validate-ui.mjs +207 -0
- package/src/agent-approval-controller.js +123 -0
- package/src/agent-context-bundles.js +242 -0
- package/src/agent-dispatch-controller.js +86 -0
- package/src/agent-memory-controller.js +374 -0
- package/src/agent-mux-client.js +280 -0
- package/src/agent-permission-review.js +162 -0
- package/src/agent-stack-controller.js +296 -0
- package/src/agent-trigger-controller.js +108 -0
- package/src/agent-workspace-controller.js +208 -0
- package/src/api-controller.js +248 -0
- package/src/argocd-gitops.js +43 -0
- package/src/auth.js +265 -0
- package/src/component-catalog.js +41 -0
- package/src/control-plane.js +136 -0
- package/src/controller-client.js +38 -0
- package/src/controller-ui.js +551 -0
- package/src/data-plane.js +178 -0
- package/src/gitea-backend.js +95 -0
- package/src/handoff.js +98 -0
- package/src/hooks-events.js +63 -0
- package/src/http-server.js +151 -0
- package/src/identity-policy.js +86 -0
- package/src/index.js +32 -0
- package/src/kubernetes-controller.js +812 -0
- package/src/kubernetes-resource-gateway.js +48 -0
- package/src/operations.js +112 -0
- package/src/resource-model.js +211 -0
- package/src/runners-ci.js +48 -0
- package/src/runtime.js +196 -0
- package/src/web-ui.js +40 -0
- package/tests/agent-approval-controller.test.js +173 -0
- package/tests/agent-context-bundles.test.js +278 -0
- package/tests/agent-dispatch-controller.test.js +176 -0
- package/tests/agent-memory-controller.test.js +308 -0
- package/tests/agent-mux-client.test.js +204 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-resources.test.js +228 -0
- package/tests/agent-stack-controller.test.js +221 -0
- package/tests/agent-trigger-controller.test.js +211 -0
- package/tests/agent-workspace-controller.test.js +215 -0
- package/tests/deployment.test.js +393 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- package/tests/krate.test.js +727 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createResource, clone } from './resource-model.js';
|
|
3
|
+
|
|
4
|
+
export const AGENT_MEMORY_CONTROLLER_BOUNDARY = {
|
|
5
|
+
role: 'agent-memory-controller',
|
|
6
|
+
scope: 'Company Brain memory management — search, snapshots, redaction, imports, time-travel',
|
|
7
|
+
owns: ['memory search', 'snapshot pinning', 'redaction scanning', 'import lifecycle', 'ontology validation'],
|
|
8
|
+
delegatesTo: ['resource-model'],
|
|
9
|
+
mustNotOwn: ['git operations', 'secret values', 'Agent Mux sessions']
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
// Redaction patterns (same as agent-context-bundles)
|
|
13
|
+
const REDACTION_PATTERNS = [
|
|
14
|
+
{ kind: 'secret-key', pattern: /(?:API_KEY|API_SECRET|SECRET_KEY|ACCESS_KEY|PRIVATE_KEY|AUTH_TOKEN|PASSWORD|PASSWD|CREDENTIALS?)\s*[=:]\s*['"]?([^\s'"}{,\]]+)/gi },
|
|
15
|
+
{ kind: 'provider-token', pattern: /\b(sk-[a-zA-Z0-9]{20,}|ghp_[a-zA-Z0-9]{36,}|gho_[a-zA-Z0-9]{36,}|glpat-[a-zA-Z0-9\-_]{20,}|xoxb-[a-zA-Z0-9\-]+|xoxp-[a-zA-Z0-9\-]+)\b/g },
|
|
16
|
+
{ kind: 'bearer-token', pattern: /Bearer\s+[a-zA-Z0-9\-._~+\/]+=*/gi },
|
|
17
|
+
{ kind: 'private-key', pattern: /-----BEGIN\s+(?:RSA\s+|EC\s+|DSA\s+|OPENSSH\s+)?PRIVATE\s+KEY-----[\s\S]*?-----END\s+(?:RSA\s+|EC\s+|DSA\s+|OPENSSH\s+)?PRIVATE\s+KEY-----/g },
|
|
18
|
+
{ kind: 'base64-credential', pattern: /\b[A-Za-z0-9+\/]{40,}={0,2}\b/g },
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function sha256(data) {
|
|
22
|
+
return createHash('sha256').update(data).digest('hex');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const IMPORT_PHASES = ['Pending', 'Collecting', 'Redacting', 'Normalizing', 'Validating', 'AwaitingReview'];
|
|
26
|
+
|
|
27
|
+
export function createAgentMemoryController(options = {}) {
|
|
28
|
+
return {
|
|
29
|
+
role: 'agent-memory-controller',
|
|
30
|
+
|
|
31
|
+
createMemorySnapshot({ memoryRepository, requestedRef, resolvedCommit, queryManifest, selectedRecords, selectedDocuments, ontologyDigest, namespace = 'default', organizationRef = 'default' }) {
|
|
32
|
+
const queryManifestDigest = sha256(JSON.stringify(queryManifest || {}));
|
|
33
|
+
const selectedRecordsDigest = sha256(JSON.stringify(selectedRecords || []));
|
|
34
|
+
const selectedDocumentsDigest = sha256(JSON.stringify(selectedDocuments || []));
|
|
35
|
+
|
|
36
|
+
const now = new Date().toISOString();
|
|
37
|
+
const snapshotName = `memsnapshot-${sha256(memoryRepository + resolvedCommit + now).slice(0, 12)}`;
|
|
38
|
+
|
|
39
|
+
const snapshot = createResource('AgentMemorySnapshot', { name: snapshotName, namespace }, {
|
|
40
|
+
organizationRef,
|
|
41
|
+
memoryRepository,
|
|
42
|
+
requestedRef,
|
|
43
|
+
resolvedCommit,
|
|
44
|
+
queryManifestDigest,
|
|
45
|
+
selectedRecordsDigest,
|
|
46
|
+
selectedDocumentsDigest,
|
|
47
|
+
ontologyDigest: ontologyDigest || '',
|
|
48
|
+
recordCount: (selectedRecords || []).length,
|
|
49
|
+
documentCount: (selectedDocuments || []).length,
|
|
50
|
+
});
|
|
51
|
+
snapshot.status = { phase: 'Pinned', createdAt: now };
|
|
52
|
+
|
|
53
|
+
return snapshot;
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
queryMemory({ snapshotRef, requester, query, records = [], documents = [], namespace = 'default', organizationRef = 'default' }) {
|
|
57
|
+
const modes = query.modes || ['graph-and-grep'];
|
|
58
|
+
const results = { graph: null, grep: null };
|
|
59
|
+
|
|
60
|
+
if (modes.includes('graph-only') || modes.includes('graph-and-grep')) {
|
|
61
|
+
results.graph = this.searchGraph({
|
|
62
|
+
records,
|
|
63
|
+
kinds: query.graph?.kinds || [],
|
|
64
|
+
edgeDepth: query.graph?.edgeDepth ?? 2,
|
|
65
|
+
query: query.text || '',
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (modes.includes('grep-only') || modes.includes('graph-and-grep')) {
|
|
70
|
+
results.grep = this.searchGrep({
|
|
71
|
+
documents,
|
|
72
|
+
paths: query.grep?.paths || [],
|
|
73
|
+
pattern: query.text || '',
|
|
74
|
+
maxMatches: query.grep?.maxMatches ?? 25,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const now = new Date().toISOString();
|
|
79
|
+
const queryName = `memquery-${sha256(snapshotRef + requester + now).slice(0, 12)}`;
|
|
80
|
+
|
|
81
|
+
const queryResource = createResource('AgentMemoryQuery', { name: queryName, namespace }, {
|
|
82
|
+
organizationRef,
|
|
83
|
+
snapshotRef,
|
|
84
|
+
requester,
|
|
85
|
+
query: clone(query),
|
|
86
|
+
resultDigest: sha256(JSON.stringify(results)),
|
|
87
|
+
});
|
|
88
|
+
queryResource.status = { phase: 'Completed', executedAt: now };
|
|
89
|
+
|
|
90
|
+
return { queryResource, results };
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
searchGraph({ records, kinds = [], edgeDepth = 2, query = '' }) {
|
|
94
|
+
let candidates = records;
|
|
95
|
+
|
|
96
|
+
// Filter by nodeKind if kinds specified
|
|
97
|
+
if (kinds.length > 0) {
|
|
98
|
+
candidates = candidates.filter(r => kinds.includes(r.nodeKind));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Match query against record.id and record.attributes (case-insensitive substring)
|
|
102
|
+
const lowerQuery = query.toLowerCase();
|
|
103
|
+
const matches = [];
|
|
104
|
+
|
|
105
|
+
for (const record of candidates) {
|
|
106
|
+
let score = 0;
|
|
107
|
+
const id = String(record.id || '').toLowerCase();
|
|
108
|
+
const attrs = JSON.stringify(record.attributes || {}).toLowerCase();
|
|
109
|
+
|
|
110
|
+
if (lowerQuery === '') {
|
|
111
|
+
score = 1;
|
|
112
|
+
} else if (id.includes(lowerQuery)) {
|
|
113
|
+
score = 2;
|
|
114
|
+
} else if (attrs.includes(lowerQuery)) {
|
|
115
|
+
score = 1;
|
|
116
|
+
} else {
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Collect edges up to edgeDepth levels
|
|
121
|
+
const edges = this._collectEdges(record, records, edgeDepth);
|
|
122
|
+
|
|
123
|
+
matches.push({ record: clone(record), score, edges });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { matches, totalMatches: matches.length };
|
|
127
|
+
},
|
|
128
|
+
|
|
129
|
+
_collectEdges(startRecord, allRecords, maxDepth) {
|
|
130
|
+
if (maxDepth <= 0 || !startRecord.edges || startRecord.edges.length === 0) return [];
|
|
131
|
+
|
|
132
|
+
const visited = new Set([startRecord.id]);
|
|
133
|
+
const collectedEdges = [];
|
|
134
|
+
let frontier = [startRecord];
|
|
135
|
+
|
|
136
|
+
for (let depth = 0; depth < maxDepth; depth++) {
|
|
137
|
+
const nextFrontier = [];
|
|
138
|
+
for (const record of frontier) {
|
|
139
|
+
for (const edge of (record.edges || [])) {
|
|
140
|
+
if (visited.has(edge.target)) continue;
|
|
141
|
+
visited.add(edge.target);
|
|
142
|
+
collectedEdges.push(clone(edge));
|
|
143
|
+
const targetRecord = allRecords.find(r => r.id === edge.target);
|
|
144
|
+
if (targetRecord) nextFrontier.push(targetRecord);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
frontier = nextFrontier;
|
|
148
|
+
if (frontier.length === 0) break;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return collectedEdges;
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
searchGrep({ documents, paths = [], pattern = '', maxMatches = 25 }) {
|
|
155
|
+
// Filter documents by path patterns (simple glob: * matches anything)
|
|
156
|
+
let filtered = documents;
|
|
157
|
+
if (paths.length > 0) {
|
|
158
|
+
filtered = documents.filter(doc => {
|
|
159
|
+
return paths.some(p => globMatch(p, doc.path));
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const excerpts = [];
|
|
164
|
+
const lowerPattern = pattern.toLowerCase();
|
|
165
|
+
|
|
166
|
+
if (lowerPattern === '') {
|
|
167
|
+
return { excerpts, totalMatches: 0 };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
for (const doc of filtered) {
|
|
171
|
+
if (excerpts.length >= maxMatches) break;
|
|
172
|
+
|
|
173
|
+
const lines = String(doc.content || '').split('\n');
|
|
174
|
+
for (let i = 0; i < lines.length; i++) {
|
|
175
|
+
if (excerpts.length >= maxMatches) break;
|
|
176
|
+
|
|
177
|
+
if (lines[i].toLowerCase().includes(lowerPattern)) {
|
|
178
|
+
const contextStart = Math.max(0, i - 1);
|
|
179
|
+
const contextEnd = Math.min(lines.length - 1, i + 1);
|
|
180
|
+
excerpts.push({
|
|
181
|
+
path: doc.path,
|
|
182
|
+
lineNumber: i + 1,
|
|
183
|
+
line: lines[i],
|
|
184
|
+
context: lines.slice(contextStart, contextEnd + 1).join('\n'),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { excerpts, totalMatches: excerpts.length };
|
|
191
|
+
},
|
|
192
|
+
|
|
193
|
+
resolveTimeTravel({ mode = 'current', requestedRef, requestedTime, commits = [] }) {
|
|
194
|
+
const now = new Date().toISOString();
|
|
195
|
+
|
|
196
|
+
if (mode === 'current') {
|
|
197
|
+
const latest = commits[0] || null;
|
|
198
|
+
return {
|
|
199
|
+
resolvedCommit: latest?.sha || latest?.id || null,
|
|
200
|
+
resolvedAt: now,
|
|
201
|
+
mode: 'current',
|
|
202
|
+
staleBy: null,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (mode === 'explicit-ref') {
|
|
207
|
+
return {
|
|
208
|
+
resolvedCommit: requestedRef,
|
|
209
|
+
resolvedAt: now,
|
|
210
|
+
mode: 'explicit-ref',
|
|
211
|
+
staleBy: null,
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (mode === 'ref-at-time') {
|
|
216
|
+
const targetTime = new Date(requestedTime).getTime();
|
|
217
|
+
let best = null;
|
|
218
|
+
for (const commit of commits) {
|
|
219
|
+
const commitTime = new Date(commit.timestamp || commit.date).getTime();
|
|
220
|
+
if (commitTime <= targetTime) {
|
|
221
|
+
if (!best || commitTime > new Date(best.timestamp || best.date).getTime()) {
|
|
222
|
+
best = commit;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const resolvedCommit = best?.sha || best?.id || null;
|
|
227
|
+
const staleBy = best ? (targetTime - new Date(best.timestamp || best.date).getTime()) : null;
|
|
228
|
+
return {
|
|
229
|
+
resolvedCommit,
|
|
230
|
+
resolvedAt: now,
|
|
231
|
+
mode: 'ref-at-time',
|
|
232
|
+
staleBy,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (mode === 'snapshot-tag') {
|
|
237
|
+
return {
|
|
238
|
+
resolvedCommit: requestedRef,
|
|
239
|
+
resolvedAt: now,
|
|
240
|
+
mode: 'snapshot-tag',
|
|
241
|
+
staleBy: null,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { resolvedCommit: null, resolvedAt: now, mode, staleBy: null };
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
createImport({ organizationRef, memoryRepository, source, include, validationPolicy, namespace = 'default' }) {
|
|
249
|
+
const now = new Date().toISOString();
|
|
250
|
+
const importName = `memimport-${sha256(memoryRepository + source + now).slice(0, 12)}`;
|
|
251
|
+
|
|
252
|
+
const importResource = createResource('AgentRunMemoryImport', { name: importName, namespace }, {
|
|
253
|
+
organizationRef,
|
|
254
|
+
memoryRepository,
|
|
255
|
+
source,
|
|
256
|
+
include: clone(include),
|
|
257
|
+
validationPolicy: validationPolicy || 'none',
|
|
258
|
+
});
|
|
259
|
+
importResource.status = { phase: 'Pending', createdAt: now };
|
|
260
|
+
|
|
261
|
+
return importResource;
|
|
262
|
+
},
|
|
263
|
+
|
|
264
|
+
processImport({ importResource, content }) {
|
|
265
|
+
const updated = clone(importResource);
|
|
266
|
+
const currentPhase = updated.status?.phase || 'Pending';
|
|
267
|
+
const now = new Date().toISOString();
|
|
268
|
+
|
|
269
|
+
const currentIndex = IMPORT_PHASES.indexOf(currentPhase);
|
|
270
|
+
if (currentIndex < 0 || currentIndex >= IMPORT_PHASES.length - 1) {
|
|
271
|
+
return updated;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const nextPhase = IMPORT_PHASES[currentIndex + 1];
|
|
275
|
+
|
|
276
|
+
// Apply phase-specific logic
|
|
277
|
+
if (nextPhase === 'Redacting') {
|
|
278
|
+
const scan = this.scanForRedaction(content || '');
|
|
279
|
+
updated.status.redactionScan = {
|
|
280
|
+
clean: scan.clean,
|
|
281
|
+
redactionCount: scan.redactionCount,
|
|
282
|
+
redactionsByKind: clone(scan.redactionsByKind),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
updated.status.phase = nextPhase;
|
|
287
|
+
updated.status.lastTransitionAt = now;
|
|
288
|
+
|
|
289
|
+
return updated;
|
|
290
|
+
},
|
|
291
|
+
|
|
292
|
+
scanForRedaction(content) {
|
|
293
|
+
if (typeof content !== 'string' || content.length === 0) {
|
|
294
|
+
return { clean: true, redactedContent: content || '', redactionCount: 0, redactionsByKind: {} };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
let redacted = content;
|
|
298
|
+
const redactionsByKind = {};
|
|
299
|
+
let redactionCount = 0;
|
|
300
|
+
|
|
301
|
+
for (const { kind, pattern } of REDACTION_PATTERNS) {
|
|
302
|
+
const fresh = new RegExp(pattern.source, pattern.flags);
|
|
303
|
+
redacted = redacted.replace(fresh, (match) => {
|
|
304
|
+
redactionsByKind[kind] = (redactionsByKind[kind] || 0) + 1;
|
|
305
|
+
redactionCount++;
|
|
306
|
+
return `[REDACTED:${kind}]`;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
clean: redactionCount === 0,
|
|
312
|
+
redactedContent: redacted,
|
|
313
|
+
redactionCount,
|
|
314
|
+
redactionsByKind,
|
|
315
|
+
};
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
validateOntology({ records, ontology }) {
|
|
319
|
+
const errors = [];
|
|
320
|
+
const requiredFields = ontology.requiredFields || {};
|
|
321
|
+
const allowedEdgeKinds = ontology.allowedEdgeKinds || [];
|
|
322
|
+
|
|
323
|
+
for (const record of records) {
|
|
324
|
+
const kind = record.nodeKind;
|
|
325
|
+
const fields = requiredFields[kind] || [];
|
|
326
|
+
|
|
327
|
+
for (const field of fields) {
|
|
328
|
+
const value = record.attributes?.[field];
|
|
329
|
+
if (value === undefined || value === null || value === '') {
|
|
330
|
+
errors.push({
|
|
331
|
+
record: record.id,
|
|
332
|
+
field,
|
|
333
|
+
message: `Missing required field '${field}' for nodeKind '${kind}'`,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Check edge kinds
|
|
339
|
+
for (const edge of (record.edges || [])) {
|
|
340
|
+
if (allowedEdgeKinds.length > 0 && !allowedEdgeKinds.includes(edge.kind)) {
|
|
341
|
+
errors.push({
|
|
342
|
+
record: record.id,
|
|
343
|
+
field: `edge.kind`,
|
|
344
|
+
message: `Edge kind '${edge.kind}' is not in allowedEdgeKinds`,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return { valid: errors.length === 0, errors };
|
|
351
|
+
},
|
|
352
|
+
|
|
353
|
+
createMemoryUpdate({ memoryRepository, sourceRun, changes, namespace = 'default', organizationRef = 'default' }) {
|
|
354
|
+
const now = new Date().toISOString();
|
|
355
|
+
const updateName = `memupdate-${sha256(memoryRepository + sourceRun + now).slice(0, 12)}`;
|
|
356
|
+
|
|
357
|
+
const update = createResource('AgentMemoryUpdate', { name: updateName, namespace }, {
|
|
358
|
+
organizationRef,
|
|
359
|
+
memoryRepository,
|
|
360
|
+
sourceRun,
|
|
361
|
+
changes: clone(changes),
|
|
362
|
+
});
|
|
363
|
+
update.status = { phase: 'Pending', createdAt: now };
|
|
364
|
+
|
|
365
|
+
return update;
|
|
366
|
+
},
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function globMatch(pattern, path) {
|
|
371
|
+
// Simple glob: convert * to regex .*
|
|
372
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
373
|
+
return new RegExp(`^${escaped}$`).test(path);
|
|
374
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import http from 'node:http';
|
|
2
|
+
import https from 'node:https';
|
|
3
|
+
import { URL } from 'node:url';
|
|
4
|
+
import { createResource } from './resource-model.js';
|
|
5
|
+
|
|
6
|
+
export const AGENT_MUX_CLIENT_BOUNDARY = {
|
|
7
|
+
role: 'agent-mux-client',
|
|
8
|
+
scope: 'HTTP/SSE adapter for Agent Mux gateway — capabilities, sessions, events, transcripts',
|
|
9
|
+
owns: ['gateway HTTP calls', 'SSE event streaming', 'transcript reconciliation'],
|
|
10
|
+
delegatesTo: ['resource-model'],
|
|
11
|
+
mustNotOwn: ['secret values', 'permission review', 'resource persistence']
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Internal HTTP request helper. Zero external deps — uses node:http / node:https.
|
|
16
|
+
* @param {string} url
|
|
17
|
+
* @param {{ method?: string, body?: object, headers?: Record<string,string>, timeout?: number }} options
|
|
18
|
+
* @returns {Promise<{ status: number, body: any }>}
|
|
19
|
+
*/
|
|
20
|
+
function httpRequest(url, { method = 'GET', body, headers = {}, timeout = 30000 } = {}) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const parsed = new URL(url);
|
|
23
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
24
|
+
const opts = {
|
|
25
|
+
hostname: parsed.hostname,
|
|
26
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
27
|
+
path: parsed.pathname + parsed.search,
|
|
28
|
+
method,
|
|
29
|
+
headers: { 'Accept': 'application/json', ...headers },
|
|
30
|
+
timeout,
|
|
31
|
+
};
|
|
32
|
+
if (body) {
|
|
33
|
+
const payload = JSON.stringify(body);
|
|
34
|
+
opts.headers['Content-Type'] = 'application/json';
|
|
35
|
+
opts.headers['Content-Length'] = Buffer.byteLength(payload);
|
|
36
|
+
}
|
|
37
|
+
const req = transport.request(opts, (res) => {
|
|
38
|
+
const chunks = [];
|
|
39
|
+
res.on('data', chunk => chunks.push(chunk));
|
|
40
|
+
res.on('end', () => {
|
|
41
|
+
const raw = Buffer.concat(chunks).toString();
|
|
42
|
+
try {
|
|
43
|
+
resolve({ status: res.statusCode, body: JSON.parse(raw) });
|
|
44
|
+
} catch {
|
|
45
|
+
resolve({ status: res.statusCode, body: raw });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
req.on('timeout', () => { req.destroy(); reject(new Error('Request timeout')); });
|
|
50
|
+
req.on('error', reject);
|
|
51
|
+
if (body) req.write(JSON.stringify(body));
|
|
52
|
+
req.end();
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Parse SSE text into an array of parsed JSON data payloads.
|
|
58
|
+
* Each `data: {...}` line is extracted; malformed JSON is silently skipped.
|
|
59
|
+
* @param {string} text
|
|
60
|
+
* @returns {object[]}
|
|
61
|
+
*/
|
|
62
|
+
export function parseSseLines(text) {
|
|
63
|
+
const events = [];
|
|
64
|
+
for (const block of text.split('\n\n')) {
|
|
65
|
+
for (const line of block.split('\n')) {
|
|
66
|
+
if (line.startsWith('data: ')) {
|
|
67
|
+
try { events.push(JSON.parse(line.slice(6))); } catch { /* skip malformed */ }
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return events;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {{ gateway?: string, enabled?: boolean }} options
|
|
76
|
+
*/
|
|
77
|
+
export function createAgentMuxClient(options = {}) {
|
|
78
|
+
const { gateway = '', enabled = false } = options;
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
role: 'agent-mux-client',
|
|
82
|
+
|
|
83
|
+
isAvailable() {
|
|
84
|
+
return enabled && !!gateway;
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Query adapter capabilities from the gateway.
|
|
89
|
+
* GET {gateway}/api/v1/agents/{adapter}/capabilities
|
|
90
|
+
* @param {string} adapter
|
|
91
|
+
* @returns {Promise<object|null>}
|
|
92
|
+
*/
|
|
93
|
+
async queryCapabilities(adapter) {
|
|
94
|
+
if (!this.isAvailable()) return null;
|
|
95
|
+
try {
|
|
96
|
+
const { status, body } = await httpRequest(`${gateway}/api/v1/agents/${encodeURIComponent(adapter)}/capabilities`);
|
|
97
|
+
if (status >= 200 && status < 300) return body;
|
|
98
|
+
return null;
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Launch a new agent session through the gateway.
|
|
106
|
+
* POST {gateway}/api/v1/sessions
|
|
107
|
+
* @param {{ stack: object, contextBundle?: object, permissionSnapshot?: object, workspace?: object }} params
|
|
108
|
+
* @returns {Promise<{ runId: string, sessionId: string }|null>}
|
|
109
|
+
*/
|
|
110
|
+
async launchSession({ stack, contextBundle, permissionSnapshot, workspace }) {
|
|
111
|
+
if (!this.isAvailable()) return null;
|
|
112
|
+
try {
|
|
113
|
+
const payload = {
|
|
114
|
+
agent: stack?.baseAgent,
|
|
115
|
+
model: stack?.model,
|
|
116
|
+
prompt: contextBundle?.prompt,
|
|
117
|
+
systemPrompt: contextBundle?.systemPrompt,
|
|
118
|
+
attachments: contextBundle?.attachments,
|
|
119
|
+
workspace: workspace?.workspacePath,
|
|
120
|
+
};
|
|
121
|
+
const { status, body } = await httpRequest(`${gateway}/api/v1/sessions`, { method: 'POST', body: payload });
|
|
122
|
+
if (status >= 200 && status < 300 && body?.runId && body?.sessionId) {
|
|
123
|
+
return { runId: body.runId, sessionId: body.sessionId };
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Get session status from the gateway.
|
|
133
|
+
* GET {gateway}/api/v1/sessions/{sessionId}
|
|
134
|
+
* @param {string} sessionId
|
|
135
|
+
* @returns {Promise<object|null>}
|
|
136
|
+
*/
|
|
137
|
+
async getSessionStatus(sessionId) {
|
|
138
|
+
if (!this.isAvailable()) return null;
|
|
139
|
+
try {
|
|
140
|
+
const { status, body } = await httpRequest(`${gateway}/api/v1/sessions/${encodeURIComponent(sessionId)}`);
|
|
141
|
+
if (status >= 200 && status < 300) return body;
|
|
142
|
+
return null;
|
|
143
|
+
} catch {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Subscribe to SSE events for a run. Reconnects with exponential backoff (1s, 2s, 4s... max 30s).
|
|
150
|
+
* GET {gateway}/api/v1/runs/{runId}/events (Accept: text/event-stream)
|
|
151
|
+
* @param {string} runId
|
|
152
|
+
* @param {(event: object) => void} callback
|
|
153
|
+
* @returns {{ abort: () => void }}
|
|
154
|
+
*/
|
|
155
|
+
subscribeToEvents(runId, callback) {
|
|
156
|
+
let aborted = false;
|
|
157
|
+
let currentReq = null;
|
|
158
|
+
let backoff = 1000;
|
|
159
|
+
|
|
160
|
+
const connect = () => {
|
|
161
|
+
if (aborted) return;
|
|
162
|
+
try {
|
|
163
|
+
const parsed = new URL(`${gateway}/api/v1/runs/${encodeURIComponent(runId)}/events`);
|
|
164
|
+
const transport = parsed.protocol === 'https:' ? https : http;
|
|
165
|
+
const opts = {
|
|
166
|
+
hostname: parsed.hostname,
|
|
167
|
+
port: parsed.port || (parsed.protocol === 'https:' ? 443 : 80),
|
|
168
|
+
path: parsed.pathname + parsed.search,
|
|
169
|
+
method: 'GET',
|
|
170
|
+
headers: { 'Accept': 'text/event-stream' },
|
|
171
|
+
};
|
|
172
|
+
currentReq = transport.request(opts, (res) => {
|
|
173
|
+
if (aborted) return;
|
|
174
|
+
// Reset backoff on successful connection
|
|
175
|
+
backoff = 1000;
|
|
176
|
+
let buffer = '';
|
|
177
|
+
res.on('data', (chunk) => {
|
|
178
|
+
if (aborted) return;
|
|
179
|
+
buffer += chunk.toString();
|
|
180
|
+
// Process complete SSE blocks (separated by double newlines)
|
|
181
|
+
const parts = buffer.split('\n\n');
|
|
182
|
+
// Keep the last part as it may be incomplete
|
|
183
|
+
buffer = parts.pop() || '';
|
|
184
|
+
for (const block of parts) {
|
|
185
|
+
for (const line of block.split('\n')) {
|
|
186
|
+
if (line.startsWith('data: ')) {
|
|
187
|
+
try {
|
|
188
|
+
callback(JSON.parse(line.slice(6)));
|
|
189
|
+
} catch { /* skip malformed */ }
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
res.on('end', () => {
|
|
195
|
+
if (!aborted) reconnect();
|
|
196
|
+
});
|
|
197
|
+
res.on('error', () => {
|
|
198
|
+
if (!aborted) reconnect();
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
currentReq.on('error', () => {
|
|
202
|
+
if (!aborted) reconnect();
|
|
203
|
+
});
|
|
204
|
+
currentReq.end();
|
|
205
|
+
} catch {
|
|
206
|
+
if (!aborted) reconnect();
|
|
207
|
+
}
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const reconnect = () => {
|
|
211
|
+
if (aborted) return;
|
|
212
|
+
const delay = backoff;
|
|
213
|
+
backoff = Math.min(backoff * 2, 30000);
|
|
214
|
+
setTimeout(connect, delay);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
connect();
|
|
218
|
+
|
|
219
|
+
return {
|
|
220
|
+
abort() {
|
|
221
|
+
aborted = true;
|
|
222
|
+
if (currentReq) {
|
|
223
|
+
currentReq.destroy();
|
|
224
|
+
currentReq = null;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Reconcile SSE events into an AgentSessionTranscript resource.
|
|
232
|
+
* Parses events by role, computes cost, creates the resource via createResource().
|
|
233
|
+
* @param {string} sessionId
|
|
234
|
+
* @param {object[]} events
|
|
235
|
+
* @param {{ namespace?: string, organizationRef?: string }} options
|
|
236
|
+
* @returns {object} AgentSessionTranscript resource
|
|
237
|
+
*/
|
|
238
|
+
reconcileTranscript(sessionId, events, { namespace = 'default', organizationRef = 'default' } = {}) {
|
|
239
|
+
const messages = [];
|
|
240
|
+
let totalInputTokens = 0;
|
|
241
|
+
let totalOutputTokens = 0;
|
|
242
|
+
|
|
243
|
+
for (const event of events) {
|
|
244
|
+
if (!event || typeof event !== 'object') continue;
|
|
245
|
+
const role = event.role || 'unknown';
|
|
246
|
+
const content = event.content || event.text || event.message || '';
|
|
247
|
+
const node = {
|
|
248
|
+
role,
|
|
249
|
+
content: typeof content === 'string' ? content : JSON.stringify(content),
|
|
250
|
+
timestamp: event.timestamp || new Date().toISOString(),
|
|
251
|
+
};
|
|
252
|
+
if (event.toolUse) node.toolUse = event.toolUse;
|
|
253
|
+
if (event.toolResult) node.toolResult = event.toolResult;
|
|
254
|
+
messages.push(node);
|
|
255
|
+
|
|
256
|
+
// Accumulate token usage if present
|
|
257
|
+
if (event.usage) {
|
|
258
|
+
totalInputTokens += event.usage.inputTokens || 0;
|
|
259
|
+
totalOutputTokens += event.usage.outputTokens || 0;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return createResource(
|
|
264
|
+
'AgentSessionTranscript',
|
|
265
|
+
{ name: `transcript-${sessionId}`, namespace },
|
|
266
|
+
{
|
|
267
|
+
organizationRef,
|
|
268
|
+
sessionRef: sessionId,
|
|
269
|
+
messages,
|
|
270
|
+
cost: {
|
|
271
|
+
inputTokens: totalInputTokens,
|
|
272
|
+
outputTokens: totalOutputTokens,
|
|
273
|
+
totalTokens: totalInputTokens + totalOutputTokens,
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
{ phase: 'Reconciled', reconciledAt: new Date().toISOString() }
|
|
277
|
+
);
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
}
|