@a5c-ai/krate 5.0.1-staging.f672fe79b
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 +29 -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 +2407 -0
- package/dist/krate-lifecycle.json +201 -0
- package/dist/krate-runtime-snapshot.json +2955 -0
- package/dist/krate-summary.json +687 -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-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/api-controller.js +206 -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 +538 -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 +30 -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 +203 -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-mux-client.test.js +204 -0
- package/tests/agent-permission-review.test.js +209 -0
- package/tests/agent-resources.test.js +212 -0
- package/tests/agent-stack-controller.test.js +221 -0
- package/tests/agent-trigger-controller.test.js +211 -0
- package/tests/deployment.test.js +395 -0
- package/tests/e2e/lifecycle.test.js +117 -0
- package/tests/krate.test.js +727 -0
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createResource, clone } from './resource-model.js';
|
|
3
|
+
|
|
4
|
+
export const AGENT_CONTEXT_BUNDLES_BOUNDARY = {
|
|
5
|
+
role: 'agent-context-bundles',
|
|
6
|
+
scope: 'Context assembly, redaction, and digest computation for agent dispatch',
|
|
7
|
+
owns: ['prompt layer collection', 'redaction patterns', 'size enforcement', 'digest computation'],
|
|
8
|
+
delegatesTo: ['resource-model'],
|
|
9
|
+
mustNotOwn: ['secret values', 'runtime execution', 'Agent Mux sessions']
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const PROMPT_LAYER_MAX = 64 * 1024; // 64 KiB
|
|
13
|
+
const BUNDLE_MAX = 750 * 1024; // 750 KiB
|
|
14
|
+
const MAX_ATTACHMENTS = 32;
|
|
15
|
+
|
|
16
|
+
// Redaction patterns (ordered by priority)
|
|
17
|
+
const REDACTION_PATTERNS = [
|
|
18
|
+
{ kind: 'secret-key', pattern: /(?:API_KEY|API_SECRET|SECRET_KEY|ACCESS_KEY|PRIVATE_KEY|AUTH_TOKEN|PASSWORD|PASSWD|CREDENTIALS?)\s*[=:]\s*['"]?([^\s'"}{,\]]+)/gi },
|
|
19
|
+
{ 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 },
|
|
20
|
+
{ kind: 'bearer-token', pattern: /Bearer\s+[a-zA-Z0-9\-._~+\/]+=*/gi },
|
|
21
|
+
{ 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 },
|
|
22
|
+
{ kind: 'base64-credential', pattern: /\b[A-Za-z0-9+\/]{40,}={0,2}\b/g },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
function sha256(data) {
|
|
26
|
+
return createHash('sha256').update(data).digest('hex');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function truncateToLimit(text, limit) {
|
|
30
|
+
if (typeof text !== 'string') return '';
|
|
31
|
+
if (text.length <= limit) return text;
|
|
32
|
+
return text.slice(0, limit);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function applyRedactions(text) {
|
|
36
|
+
if (typeof text !== 'string' || text.length === 0) return { text, counts: {} };
|
|
37
|
+
const counts = {};
|
|
38
|
+
let redacted = text;
|
|
39
|
+
for (const { kind, pattern } of REDACTION_PATTERNS) {
|
|
40
|
+
const fresh = new RegExp(pattern.source, pattern.flags);
|
|
41
|
+
const before = redacted;
|
|
42
|
+
redacted = redacted.replace(fresh, (match) => {
|
|
43
|
+
counts[kind] = (counts[kind] || 0) + 1;
|
|
44
|
+
return `[REDACTED:${kind}]`;
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return { text: redacted, counts };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function mergeRedactionCounts(target, source) {
|
|
51
|
+
for (const [kind, count] of Object.entries(source)) {
|
|
52
|
+
target[kind] = (target[kind] || 0) + count;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createRedactionManifest(redactionCounts) {
|
|
57
|
+
const total = Object.values(redactionCounts).reduce((sum, n) => sum + n, 0);
|
|
58
|
+
return { total, byKind: clone(redactionCounts) };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function assembleContextBundle({ stack, repository, ref, sourceRefs = [], contextLabels = [], redactionPolicy, resources = {} }) {
|
|
62
|
+
const namespace = stack?.metadata?.namespace || 'default';
|
|
63
|
+
const organizationRef = stack?.spec?.organizationRef || 'default';
|
|
64
|
+
const allRedactionCounts = {};
|
|
65
|
+
|
|
66
|
+
// Step 1 — Collect prompt layers
|
|
67
|
+
const rawSystem = stack?.spec?.prompt?.system || '';
|
|
68
|
+
const rawDeveloper = stack?.spec?.prompt?.developer || '';
|
|
69
|
+
const rawTask = stack?.spec?.prompt?.task || '';
|
|
70
|
+
|
|
71
|
+
// Collect skill fragments
|
|
72
|
+
const skillFragments = [];
|
|
73
|
+
const skillRefs = stack?.spec?.skillRefs || [];
|
|
74
|
+
const agentSkills = resources.AgentSkill || [];
|
|
75
|
+
for (const skillRef of skillRefs) {
|
|
76
|
+
const skill = agentSkills.find((s) => s.metadata?.name === skillRef);
|
|
77
|
+
if (skill?.spec?.promptFragment) {
|
|
78
|
+
skillFragments.push({ name: skillRef, content: skill.spec.promptFragment });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Collect label fragments
|
|
83
|
+
const labelFragments = [];
|
|
84
|
+
const agentContextLabels = resources.AgentContextLabel || [];
|
|
85
|
+
for (const labelRef of contextLabels) {
|
|
86
|
+
const label = agentContextLabels.find((l) => l.metadata?.name === labelRef);
|
|
87
|
+
if (label?.spec?.promptFragment) {
|
|
88
|
+
labelFragments.push({ name: labelRef, content: label.spec.promptFragment });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Step 2 — Truncate each layer and compute digests
|
|
93
|
+
const system = truncateToLimit(rawSystem, PROMPT_LAYER_MAX);
|
|
94
|
+
const developer = truncateToLimit(rawDeveloper, PROMPT_LAYER_MAX);
|
|
95
|
+
const task = truncateToLimit(rawTask, PROMPT_LAYER_MAX);
|
|
96
|
+
|
|
97
|
+
// Apply redaction to prompt layers
|
|
98
|
+
const systemRedacted = applyRedactions(system);
|
|
99
|
+
mergeRedactionCounts(allRedactionCounts, systemRedacted.counts);
|
|
100
|
+
const developerRedacted = applyRedactions(developer);
|
|
101
|
+
mergeRedactionCounts(allRedactionCounts, developerRedacted.counts);
|
|
102
|
+
const taskRedacted = applyRedactions(task);
|
|
103
|
+
mergeRedactionCounts(allRedactionCounts, taskRedacted.counts);
|
|
104
|
+
|
|
105
|
+
const systemDigest = sha256(systemRedacted.text);
|
|
106
|
+
const developerDigest = sha256(developerRedacted.text);
|
|
107
|
+
const taskDigest = sha256(taskRedacted.text);
|
|
108
|
+
|
|
109
|
+
const skillLayerDigests = [];
|
|
110
|
+
const redactedSkillFragments = [];
|
|
111
|
+
for (const frag of skillFragments) {
|
|
112
|
+
const truncated = truncateToLimit(frag.content, PROMPT_LAYER_MAX);
|
|
113
|
+
const redacted = applyRedactions(truncated);
|
|
114
|
+
mergeRedactionCounts(allRedactionCounts, redacted.counts);
|
|
115
|
+
const digest = sha256(redacted.text);
|
|
116
|
+
skillLayerDigests.push({ role: `skill:${frag.name}`, digest, sizeBytes: redacted.text.length });
|
|
117
|
+
redactedSkillFragments.push({ name: frag.name, content: redacted.text });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const labelLayerDigests = [];
|
|
121
|
+
const redactedLabelFragments = [];
|
|
122
|
+
for (const frag of labelFragments) {
|
|
123
|
+
const truncated = truncateToLimit(frag.content, PROMPT_LAYER_MAX);
|
|
124
|
+
const redacted = applyRedactions(truncated);
|
|
125
|
+
mergeRedactionCounts(allRedactionCounts, redacted.counts);
|
|
126
|
+
const digest = sha256(redacted.text);
|
|
127
|
+
labelLayerDigests.push({ role: `label:${frag.name}`, digest, sizeBytes: redacted.text.length });
|
|
128
|
+
redactedLabelFragments.push({ name: frag.name, content: redacted.text });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Step 3 — Collect sources from sourceRefs
|
|
132
|
+
const sources = [];
|
|
133
|
+
const limitedSourceRefs = sourceRefs.slice(0, MAX_ATTACHMENTS);
|
|
134
|
+
for (const srcRef of limitedSourceRefs) {
|
|
135
|
+
const content = truncateToLimit(srcRef.content || '', PROMPT_LAYER_MAX);
|
|
136
|
+
const redacted = applyRedactions(content);
|
|
137
|
+
mergeRedactionCounts(allRedactionCounts, redacted.counts);
|
|
138
|
+
sources.push({
|
|
139
|
+
kind: srcRef.kind || 'unknown',
|
|
140
|
+
ref: srcRef.ref || '',
|
|
141
|
+
content: redacted.text,
|
|
142
|
+
digest: sha256(redacted.text)
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Step 5 — Enforce total size
|
|
147
|
+
let wasTruncated = false;
|
|
148
|
+
const TRUNC_MARKER = '[...truncated at 750KiB limit]';
|
|
149
|
+
const TRUNC_MARKER_LEN = TRUNC_MARKER.length;
|
|
150
|
+
|
|
151
|
+
const computeTotalSize = () =>
|
|
152
|
+
systemRedacted.text.length + developerRedacted.text.length + taskRedacted.text.length +
|
|
153
|
+
redactedSkillFragments.reduce((s, f) => s + f.content.length, 0) +
|
|
154
|
+
redactedLabelFragments.reduce((s, f) => s + f.content.length, 0) +
|
|
155
|
+
sources.reduce((s, src) => s + src.content.length, 0);
|
|
156
|
+
|
|
157
|
+
let totalSize = computeTotalSize();
|
|
158
|
+
|
|
159
|
+
if (totalSize > BUNDLE_MAX) {
|
|
160
|
+
wasTruncated = true;
|
|
161
|
+
// Build a list of truncatable items (sources first, then labels, then skills)
|
|
162
|
+
// sorted by descending size so we cut the largest first
|
|
163
|
+
const truncatableItems = [
|
|
164
|
+
...sources.map((s, i) => ({ type: 'source', index: i })),
|
|
165
|
+
...redactedLabelFragments.map((f, i) => ({ type: 'label', index: i })),
|
|
166
|
+
...redactedSkillFragments.map((f, i) => ({ type: 'skill', index: i }))
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
const getContent = (t) => {
|
|
170
|
+
if (t.type === 'source') return sources[t.index].content;
|
|
171
|
+
if (t.type === 'label') return redactedLabelFragments[t.index].content;
|
|
172
|
+
return redactedSkillFragments[t.index].content;
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
truncatableItems.sort((a, b) => getContent(b).length - getContent(a).length);
|
|
176
|
+
|
|
177
|
+
for (const target of truncatableItems) {
|
|
178
|
+
totalSize = computeTotalSize();
|
|
179
|
+
if (totalSize <= BUNDLE_MAX) break;
|
|
180
|
+
|
|
181
|
+
const currentContent = getContent(target);
|
|
182
|
+
const excess = totalSize - BUNDLE_MAX;
|
|
183
|
+
// We need to remove at least `excess` bytes, but also account for the marker we add
|
|
184
|
+
const cutAmount = excess + TRUNC_MARKER_LEN;
|
|
185
|
+
const newLen = Math.max(0, currentContent.length - cutAmount);
|
|
186
|
+
const truncatedContent = currentContent.slice(0, newLen) + TRUNC_MARKER;
|
|
187
|
+
|
|
188
|
+
if (target.type === 'source') {
|
|
189
|
+
sources[target.index].content = truncatedContent;
|
|
190
|
+
sources[target.index].digest = sha256(truncatedContent);
|
|
191
|
+
} else if (target.type === 'label') {
|
|
192
|
+
redactedLabelFragments[target.index].content = truncatedContent;
|
|
193
|
+
labelLayerDigests[target.index].digest = sha256(truncatedContent);
|
|
194
|
+
labelLayerDigests[target.index].sizeBytes = truncatedContent.length;
|
|
195
|
+
} else if (target.type === 'skill') {
|
|
196
|
+
redactedSkillFragments[target.index].content = truncatedContent;
|
|
197
|
+
skillLayerDigests[target.index].digest = sha256(truncatedContent);
|
|
198
|
+
skillLayerDigests[target.index].sizeBytes = truncatedContent.length;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Step 6 — Compute bundle digest
|
|
204
|
+
const promptLayers = [
|
|
205
|
+
{ role: 'system', digest: systemDigest, sizeBytes: systemRedacted.text.length },
|
|
206
|
+
{ role: 'developer', digest: developerDigest, sizeBytes: developerRedacted.text.length },
|
|
207
|
+
{ role: 'task', digest: taskDigest, sizeBytes: taskRedacted.text.length },
|
|
208
|
+
...skillLayerDigests,
|
|
209
|
+
...labelLayerDigests,
|
|
210
|
+
];
|
|
211
|
+
|
|
212
|
+
const digestPayload = JSON.stringify({
|
|
213
|
+
promptLayers,
|
|
214
|
+
sources: sources.map((s) => ({ kind: s.kind, ref: s.ref, digest: s.digest }))
|
|
215
|
+
});
|
|
216
|
+
const digest = sha256(digestPayload);
|
|
217
|
+
|
|
218
|
+
const redactionManifest = createRedactionManifest(allRedactionCounts);
|
|
219
|
+
|
|
220
|
+
// Step 7 — Build the resource
|
|
221
|
+
const resource = createResource('AgentContextBundle', { name: `bundle-${digest.slice(0, 12)}`, namespace }, {
|
|
222
|
+
organizationRef,
|
|
223
|
+
dispatchRun: '',
|
|
224
|
+
digest,
|
|
225
|
+
sources: limitedSourceRefs.map((s) => ({ kind: s.kind || 'unknown', ref: s.ref || '' })),
|
|
226
|
+
promptLayers,
|
|
227
|
+
redactions: redactionManifest,
|
|
228
|
+
limits: { maxBytes: BUNDLE_MAX, truncated: wasTruncated },
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Store actual text content in _content (in-memory only, not part of resource spec)
|
|
232
|
+
resource._content = {
|
|
233
|
+
system: systemRedacted.text,
|
|
234
|
+
developer: developerRedacted.text,
|
|
235
|
+
task: taskRedacted.text,
|
|
236
|
+
skillFragments: redactedSkillFragments,
|
|
237
|
+
labelFragments: redactedLabelFragments,
|
|
238
|
+
sources
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
return resource;
|
|
242
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createPermissionReviewer } from './agent-permission-review.js';
|
|
2
|
+
import { createAgentStackController } from './agent-stack-controller.js';
|
|
3
|
+
import { assembleContextBundle } from './agent-context-bundles.js';
|
|
4
|
+
import { createResource, clone } from './resource-model.js';
|
|
5
|
+
import { createAgentMuxClient } from './agent-mux-client.js';
|
|
6
|
+
|
|
7
|
+
export const AGENT_DISPATCH_CONTROLLER_BOUNDARY = {
|
|
8
|
+
role: 'agent-dispatch-controller',
|
|
9
|
+
scope: 'Manual dispatch orchestration with permission gating and context assembly',
|
|
10
|
+
owns: ['dispatch creation', 'attempt lifecycle', 'Agent Mux session binding'],
|
|
11
|
+
delegatesTo: ['agent-permission-review', 'agent-stack-controller', 'agent-context-bundles', 'agent-mux-client'],
|
|
12
|
+
mustNotOwn: ['secret values', 'UI rendering']
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export function createAgentDispatchController(options = {}) {
|
|
16
|
+
const permissionReviewer = options.permissionReviewer || createPermissionReviewer();
|
|
17
|
+
const stackController = options.stackController || createAgentStackController();
|
|
18
|
+
const agentMuxClient = options.agentMuxClient || createAgentMuxClient();
|
|
19
|
+
|
|
20
|
+
return {
|
|
21
|
+
role: 'agent-dispatch-controller',
|
|
22
|
+
|
|
23
|
+
async createManualDispatch({ repository, ref, sourceRefs = [], agentStack, taskKind, actor, namespace = 'default', organizationRef = 'default', resources = {} }) {
|
|
24
|
+
// 1. Find stack
|
|
25
|
+
const stack = (resources.AgentStack || []).find(s => s.metadata?.name === agentStack);
|
|
26
|
+
if (!stack) return { error: true, reason: 'stack-not-found', message: `AgentStack '${agentStack}' not found` };
|
|
27
|
+
|
|
28
|
+
// 2. Permission review
|
|
29
|
+
const review = permissionReviewer.reviewPermissions({ repository, ref, actor, agentStack, resources });
|
|
30
|
+
if (review.decision === 'denied') {
|
|
31
|
+
return { error: true, reason: 'permission-denied', message: 'Dispatch denied by permission review', review };
|
|
32
|
+
}
|
|
33
|
+
const permissionSnapshot = permissionReviewer.createPermissionSnapshot(review);
|
|
34
|
+
|
|
35
|
+
// 3. Assemble context bundle
|
|
36
|
+
const contextBundle = assembleContextBundle({ stack, repository, ref, sourceRefs, contextLabels: [], resources });
|
|
37
|
+
|
|
38
|
+
// 4. Create resources
|
|
39
|
+
const now = new Date().toISOString();
|
|
40
|
+
const runName = `dispatch-${Date.now()}`;
|
|
41
|
+
|
|
42
|
+
const run = createResource('AgentDispatchRun', { name: runName, namespace }, {
|
|
43
|
+
organizationRef,
|
|
44
|
+
repository,
|
|
45
|
+
sourceRefs: clone(sourceRefs),
|
|
46
|
+
agentStack,
|
|
47
|
+
taskKind: taskKind || 'diagnostic',
|
|
48
|
+
contextBundleRef: contextBundle.metadata.name,
|
|
49
|
+
});
|
|
50
|
+
run.status = { phase: 'Pending', queuedAt: now };
|
|
51
|
+
|
|
52
|
+
const attempt = createResource('AgentDispatchAttempt', { name: `${runName}-attempt-1`, namespace }, {
|
|
53
|
+
organizationRef,
|
|
54
|
+
agentDispatchRun: runName,
|
|
55
|
+
attemptReason: 'initial',
|
|
56
|
+
agentStackSnapshot: clone(stack.spec),
|
|
57
|
+
contextBundleDigest: contextBundle.spec.digest,
|
|
58
|
+
});
|
|
59
|
+
attempt.status = { permissionSnapshot, queueEnteredAt: now };
|
|
60
|
+
|
|
61
|
+
// 5. Try Agent Mux launch
|
|
62
|
+
if (agentMuxClient.isAvailable()) {
|
|
63
|
+
try {
|
|
64
|
+
const session = await agentMuxClient.launchSession({ stack, contextBundle, permissionSnapshot });
|
|
65
|
+
if (session && session.runId) {
|
|
66
|
+
attempt.status.agentMuxRunId = session.runId;
|
|
67
|
+
attempt.status.agentMuxSessionId = session.sessionId;
|
|
68
|
+
run.status.phase = 'Running';
|
|
69
|
+
attempt.status.startedAt = now;
|
|
70
|
+
} else {
|
|
71
|
+
run.status.phase = 'Queued';
|
|
72
|
+
run.status.conditions = [{ type: 'AgentMuxBound', status: 'False', reason: 'LaunchFailed', message: 'Agent Mux launch returned no session' }];
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
run.status.phase = 'Queued';
|
|
76
|
+
run.status.conditions = [{ type: 'AgentMuxBound', status: 'False', reason: 'LaunchFailed' }];
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
run.status.phase = 'Queued';
|
|
80
|
+
run.status.conditions = [{ type: 'AgentMuxBound', status: 'False', reason: 'Unavailable', message: 'Agent Mux gateway not configured' }];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return { error: false, run, attempt, contextBundle, permissionSnapshot };
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -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
|
+
}
|