@growthub/cli 0.13.9 → 0.14.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/env-status/route.js +31 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/apply/route.js +227 -5
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/helper/query/route.js +1 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/api/workspace/sandbox-run/route.js +70 -9
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceActivationPanel.jsx +17 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/components/WorkspaceHelperSetupModal.jsx +6 -3
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/AgentSwarmPanel.jsx +61 -35
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ApiRegistryCreationCockpit.jsx +200 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/DataModelShell.jsx +414 -9
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/HelperSidecar.jsx +339 -77
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationGraphCanvas.jsx +81 -10
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/OrchestrationNodeConfigPanel.jsx +70 -85
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/ReferencePicker.jsx +2 -2
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SidecarExpandView.jsx +37 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/SwarmRunCockpit.jsx +625 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/data-model/components/helper-commands.js +150 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/globals.css +229 -9
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/app/workflows/WorkflowSurface.jsx +224 -14
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/integrations/resolver-loader.js +2 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-agent-host.js +139 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/adapters/sandboxes/default-local-intelligence.js +4 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-registry-creation-flow.js +317 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/api-response-profile.js +207 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/creation-error-recovery.js +103 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/env-status.js +100 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-agent-swarm.js +246 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-graph.js +69 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/orchestration-run-console.js +411 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/sandbox-serverless-flow.js +215 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/server-resolver-write.js +67 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/serverless-upgrade.js +89 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-activation.js +11 -4
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-data-model.js +8 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-helper.js +30 -1
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-metadata-store.js +8 -6
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-resolver-proposal.js +200 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/lib/workspace-swarm-proposal.js +551 -0
- package/assets/worker-kits/growthub-custom-workspace-starter-v1/apps/workspace/package.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server Resolver Write V1 — the single, confined, gated fs write for resolver
|
|
3
|
+
* files. Used by the helper apply route's resolver.create lane (and available
|
|
4
|
+
* to register-resolver). Server-only.
|
|
5
|
+
*
|
|
6
|
+
* AWaC topology: writes only in filesystem mode; read-only runtimes throw a
|
|
7
|
+
* coded error with guidance (the same 409 contract the rest of the workspace
|
|
8
|
+
* uses). Path is confined to lib/adapters/integrations/resolvers — never
|
|
9
|
+
* escapes. Never logs file contents.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { promises as fs } from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { describePersistenceMode } from "@/lib/workspace-config";
|
|
15
|
+
import { resolveResolverFilePath } from "@/lib/workspace-resolver-proposal";
|
|
16
|
+
|
|
17
|
+
const MAX_RESOLVER_SIZE = 256 * 1024;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Write a resolver file from a validated proposal. Returns
|
|
21
|
+
* { saved:true, path, filename } or throws a coded error:
|
|
22
|
+
* - WORKSPACE_PERSISTENCE_READ_ONLY (read-only runtime; carries guidance)
|
|
23
|
+
* - INVALID_RESOLVER_WRITE (bad path / code)
|
|
24
|
+
* - WORKSPACE_PERSISTENCE_PATH_REFUSED (escaped the resolver dir)
|
|
25
|
+
*/
|
|
26
|
+
async function writeResolverProposalFile(proposal) {
|
|
27
|
+
const persistence = describePersistenceMode();
|
|
28
|
+
if (!persistence.canSave) {
|
|
29
|
+
const error = new Error(persistence.reason || "resolver write requires a writable filesystem runtime");
|
|
30
|
+
error.code = "WORKSPACE_PERSISTENCE_READ_ONLY";
|
|
31
|
+
error.guidance = persistence.guidance || "Set WORKSPACE_CONFIG_ALLOW_FS_WRITE=true or use local development mode.";
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
const target = (proposal?.target && proposal.target.ok)
|
|
35
|
+
? proposal.target
|
|
36
|
+
: resolveResolverFilePath(proposal?.payload?.integrationId);
|
|
37
|
+
if (!target || !target.ok) {
|
|
38
|
+
const error = new Error(target?.error || "invalid resolver target path");
|
|
39
|
+
error.code = "INVALID_RESOLVER_WRITE";
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
const code = String(proposal?.code || "");
|
|
43
|
+
if (!code.includes("registerSourceResolver")) {
|
|
44
|
+
const error = new Error("resolver code must call registerSourceResolver()");
|
|
45
|
+
error.code = "INVALID_RESOLVER_WRITE";
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
if (Buffer.byteLength(code, "utf8") > MAX_RESOLVER_SIZE) {
|
|
49
|
+
const error = new Error(`resolver file must be smaller than ${MAX_RESOLVER_SIZE / 1024} KB`);
|
|
50
|
+
error.code = "INVALID_RESOLVER_WRITE";
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const resolversDir = path.resolve(/*turbopackIgnore: true*/ process.cwd(), target.dir);
|
|
55
|
+
const outPath = path.join(resolversDir, target.filename);
|
|
56
|
+
if (path.dirname(outPath) !== resolversDir) {
|
|
57
|
+
const error = new Error("invalid filename — path traversal not allowed");
|
|
58
|
+
error.code = "WORKSPACE_PERSISTENCE_PATH_REFUSED";
|
|
59
|
+
throw error;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
await fs.mkdir(resolversDir, { recursive: true });
|
|
63
|
+
await fs.writeFile(outPath, code, "utf8");
|
|
64
|
+
return { saved: true, path: target.path, filename: target.filename };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { writeResolverProposalFile };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serverless Upgrade Onboarding V1 — derives the one-time "upgrade your
|
|
3
|
+
* localhost workflow to a persistent, scheduled serverless workflow" nudge from
|
|
4
|
+
* the workspace configuration state, following the same one-time onboarding
|
|
5
|
+
* pattern as the lens walkthrough (workspace-ui-cache dismiss flag).
|
|
6
|
+
*
|
|
7
|
+
* It reads only the existing model: sandbox-environment rows and their
|
|
8
|
+
* `runLocality` field (already part of the governed schema). When the operator
|
|
9
|
+
* has workflows but none are serverless — and hasn't dismissed the nudge — the
|
|
10
|
+
* upgrade path shows once.
|
|
11
|
+
*
|
|
12
|
+
* Pure + deterministic; never throws. The dismiss flag is read by the caller
|
|
13
|
+
* from the governed workspace-ui-cache row and passed in.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const SERVERLESS_UPGRADE_DISMISS_FLAG = "serverlessUpgradeDismissed";
|
|
17
|
+
const UPGRADE_STATE_KIND = "growthub-serverless-upgrade-state-v1";
|
|
18
|
+
|
|
19
|
+
function isPlainObject(value) {
|
|
20
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function clean(value) {
|
|
24
|
+
return String(value == null ? "" : value).trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Is a sandbox row configured to run serverless? */
|
|
28
|
+
function rowIsServerless(row) {
|
|
29
|
+
return isPlainObject(row) && clean(row.runLocality).toLowerCase() === "serverless";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Collect every sandbox-environment (workflow) row across the data model. */
|
|
33
|
+
function collectWorkflowRows(workspaceConfig) {
|
|
34
|
+
const objects = Array.isArray(workspaceConfig?.dataModel?.objects) ? workspaceConfig.dataModel.objects : [];
|
|
35
|
+
const rows = [];
|
|
36
|
+
for (const object of objects) {
|
|
37
|
+
if (object?.objectType !== "sandbox-environment") continue;
|
|
38
|
+
for (const row of Array.isArray(object.rows) ? object.rows : []) {
|
|
39
|
+
if (isPlainObject(row)) rows.push(row);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return rows;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Derive the serverless-upgrade onboarding state.
|
|
47
|
+
*
|
|
48
|
+
* @param {object} workspaceConfig
|
|
49
|
+
* @param {object} [options]
|
|
50
|
+
* @param {boolean} [options.dismissed] dismiss flag from workspace-ui-cache
|
|
51
|
+
*/
|
|
52
|
+
function deriveServerlessUpgradeState(workspaceConfig, options = {}) {
|
|
53
|
+
const opts = isPlainObject(options) ? options : {};
|
|
54
|
+
const rows = collectWorkflowRows(workspaceConfig);
|
|
55
|
+
const serverlessCount = rows.filter(rowIsServerless).length;
|
|
56
|
+
const localCount = rows.length - serverlessCount;
|
|
57
|
+
const hasWorkflows = rows.length > 0;
|
|
58
|
+
const dismissed = opts.dismissed === true || String(opts.dismissed || "") === "true";
|
|
59
|
+
// Show the one-time path only when the operator has built workflows but none
|
|
60
|
+
// are serverless yet — the moment the upgrade mental model is most useful.
|
|
61
|
+
const showOnboarding = hasWorkflows && serverlessCount === 0 && !dismissed;
|
|
62
|
+
return {
|
|
63
|
+
kind: UPGRADE_STATE_KIND,
|
|
64
|
+
version: 1,
|
|
65
|
+
hasWorkflows,
|
|
66
|
+
workflowCount: rows.length,
|
|
67
|
+
serverlessCount,
|
|
68
|
+
localCount,
|
|
69
|
+
allLocal: hasWorkflows && serverlessCount === 0,
|
|
70
|
+
dismissed,
|
|
71
|
+
showOnboarding,
|
|
72
|
+
headline: showOnboarding
|
|
73
|
+
? "Upgrade a workflow to serverless"
|
|
74
|
+
: serverlessCount > 0
|
|
75
|
+
? "Serverless workflows are active"
|
|
76
|
+
: "Workflows run locally",
|
|
77
|
+
subheadline: showOnboarding
|
|
78
|
+
? "Make it persistent and scheduled — it runs on invocation through your configured adapter, not just on this machine."
|
|
79
|
+
: "",
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export {
|
|
84
|
+
SERVERLESS_UPGRADE_DISMISS_FLAG,
|
|
85
|
+
UPGRADE_STATE_KIND,
|
|
86
|
+
rowIsServerless,
|
|
87
|
+
collectWorkflowRows,
|
|
88
|
+
deriveServerlessUpgradeState,
|
|
89
|
+
};
|
|
@@ -405,9 +405,15 @@ function deriveBlankWorkspaceActivationState({ workspaceConfig, workspaceSourceR
|
|
|
405
405
|
}, 0);
|
|
406
406
|
const widgetAdded = widgetCount > 0;
|
|
407
407
|
|
|
408
|
-
const workflowMatch = findWorkflowRow(workspaceConfig, () =>
|
|
408
|
+
const workflowMatch = findWorkflowRow(workspaceConfig, (_row, object) => safeString(object?.id).trim() !== "workspace-helper-sandbox");
|
|
409
409
|
const workflowCreated = Boolean(workflowMatch?.row);
|
|
410
410
|
const workflowRun = deriveLatestRunStatus(workflowMatch?.row);
|
|
411
|
+
const workflowObjectId = safeString(workflowMatch?.object?.id).trim();
|
|
412
|
+
const workflowRowName = safeString(workflowMatch?.row?.Name || workflowMatch?.row?.name || workflowMatch?.row?.slug || workflowMatch?.row?.id).trim();
|
|
413
|
+
const workflowField = workflowMatch?.row?.orchestrationConfig !== undefined ? "orchestrationConfig" : "orchestrationGraph";
|
|
414
|
+
const workflowHref = workflowObjectId && workflowRowName
|
|
415
|
+
? `/workflows?object=${encodeURIComponent(workflowObjectId)}&row=${encodeURIComponent(workflowRowName)}&field=${encodeURIComponent(workflowField)}`
|
|
416
|
+
: "";
|
|
411
417
|
|
|
412
418
|
const steps = [
|
|
413
419
|
{
|
|
@@ -449,8 +455,9 @@ function deriveBlankWorkspaceActivationState({ workspaceConfig, workspaceSourceR
|
|
|
449
455
|
? "Sandbox workflow scaffolded."
|
|
450
456
|
: "Open Workflows to assemble your first automation.",
|
|
451
457
|
status: workflowCreated ? "complete" : "pending",
|
|
452
|
-
href: "
|
|
453
|
-
|
|
458
|
+
href: workflowCreated ? workflowHref : "",
|
|
459
|
+
action: workflowCreated ? "" : "create-workflow",
|
|
460
|
+
cta: workflowCreated ? "Open workflow" : "New workflow",
|
|
454
461
|
},
|
|
455
462
|
{
|
|
456
463
|
id: "run-workflow",
|
|
@@ -461,7 +468,7 @@ function deriveBlankWorkspaceActivationState({ workspaceConfig, workspaceSourceR
|
|
|
461
468
|
? "Last run failed — open the trace and fix the failing node."
|
|
462
469
|
: "Click Test inside the workflow to do a first run.",
|
|
463
470
|
status: workflowRun.ok ? "complete" : (workflowCreated ? "pending" : "blocked"),
|
|
464
|
-
href: "
|
|
471
|
+
href: workflowCreated ? workflowHref : "",
|
|
465
472
|
hint: workflowRun.ok || workflowCreated ? "" : "Create a workflow first.",
|
|
466
473
|
cta: workflowRun.ok ? "View runs" : "Open workflow",
|
|
467
474
|
},
|
|
@@ -937,6 +937,13 @@ function createTypedBusinessObject(workspaceConfig, { name, objectType = "custom
|
|
|
937
937
|
? workspaceConfig.dataModel
|
|
938
938
|
: {};
|
|
939
939
|
const id = uniqueObjectId(workspaceConfig, label);
|
|
940
|
+
const relations = preset.relations
|
|
941
|
+
? preset.relations.map((relation) => {
|
|
942
|
+
const next = { ...relation };
|
|
943
|
+
if (next.statusAllowlist == null) delete next.statusAllowlist;
|
|
944
|
+
return next;
|
|
945
|
+
})
|
|
946
|
+
: [];
|
|
940
947
|
const object = {
|
|
941
948
|
id,
|
|
942
949
|
label,
|
|
@@ -946,7 +953,7 @@ function createTypedBusinessObject(workspaceConfig, { name, objectType = "custom
|
|
|
946
953
|
columns,
|
|
947
954
|
rows: [],
|
|
948
955
|
binding: { mode: "manual", source: "Data Model" },
|
|
949
|
-
relations
|
|
956
|
+
relations,
|
|
950
957
|
fieldSettings: normalizeFieldSettings({}, columns)
|
|
951
958
|
};
|
|
952
959
|
return {
|
|
@@ -37,6 +37,15 @@ const WORKSPACE_HELPER_PROPOSAL_TYPES = [
|
|
|
37
37
|
"dataModel.row.add",
|
|
38
38
|
"repair.binding",
|
|
39
39
|
"explain.object",
|
|
40
|
+
// Server-file lane (AWaC: NOT a config PATCH field). Routed in helper/apply to
|
|
41
|
+
// the confined, gated resolver write — never through writeWorkspaceConfig.
|
|
42
|
+
"resolver.create",
|
|
43
|
+
// Swarm lane — governed sandbox-environment rows in the EXISTING dataModel
|
|
44
|
+
// patch field. Apply normalizes the intent payload into an agent-swarm-v1
|
|
45
|
+
// graph via buildDefaultAgentSwarmGraph; execution stays behind sandbox-run.
|
|
46
|
+
"swarm.run.propose",
|
|
47
|
+
"swarm.workflow.save",
|
|
48
|
+
"swarm.run.resume",
|
|
40
49
|
];
|
|
41
50
|
|
|
42
51
|
const PROPOSAL_TYPE_TO_PATCH_FIELD = {
|
|
@@ -50,6 +59,13 @@ const PROPOSAL_TYPE_TO_PATCH_FIELD = {
|
|
|
50
59
|
"dataModel.row.add": "dataModel",
|
|
51
60
|
"repair.binding": "dataModel",
|
|
52
61
|
"explain.object": "dataModel",
|
|
62
|
+
// Sentinel — resolver.create writes a server file, not a config field. It is
|
|
63
|
+
// explicitly excluded from the PATCH allowlist and handled by its own lane.
|
|
64
|
+
"resolver.create": "server-file",
|
|
65
|
+
// Swarm rows are dataModel rows. No new PATCH field is introduced.
|
|
66
|
+
"swarm.run.propose": "dataModel",
|
|
67
|
+
"swarm.workflow.save": "dataModel",
|
|
68
|
+
"swarm.run.resume": "dataModel",
|
|
53
69
|
};
|
|
54
70
|
|
|
55
71
|
const KNOWN_WIDGET_KINDS = ["chart", "view", "iframe", "rich-text"];
|
|
@@ -62,7 +78,7 @@ const INTENT_DESCRIPTIONS = {
|
|
|
62
78
|
create_widget:
|
|
63
79
|
"Suggest which widgetTypes belong on the workspace based on the object schema and target KPIs. Propose widgetType entries and canvas widget placements.",
|
|
64
80
|
register_api:
|
|
65
|
-
"Draft API Registry rows (dataModel object of objectType api-registry) including integration labels, credential prompts, base URL, endpoint, auth header, and method.",
|
|
81
|
+
"Draft API Registry rows (dataModel object of objectType api-registry) including integration labels, credential prompts, base URL, endpoint, auth header, and method. When the user wants the response shaped into governed rows (a Data Source) or describes a nested/paginated response, also recommend a resolver in the `summary` and propose the matching data-source object (objectType data-source) that references the registry row by registryId — guiding them into the resolver / Data Source lane. Credentials are runtime env keys (REF / REF_API_KEY / REF_TOKEN in .env.local), never stored in config.",
|
|
66
82
|
create_object:
|
|
67
83
|
"Translate the user's domain language into a new dataModel object: objectType, label, columns, starter rows, and field settings that make sense for their business.",
|
|
68
84
|
edit_view:
|
|
@@ -71,6 +87,8 @@ const INTENT_DESCRIPTIONS = {
|
|
|
71
87
|
"Inspect the workspace snapshot for broken references, missing bindings, empty objects, or incomplete views. Propose the minimum changes needed to repair each issue.",
|
|
72
88
|
explain:
|
|
73
89
|
"Return a clear explanation of what one or more workspace objects, widgets, or configurations do. Use the explain.object proposal type — payload is { explanation: string }.",
|
|
90
|
+
swarm:
|
|
91
|
+
"Propose a governed agent swarm via swarm.run.propose. You are propose-only: do not execute, do not store credentials. Describe the swarm objective, agent roles, task prompts, tools, maxConcurrency, and outcomeCriteria as an intent payload — the server normalizes it into the governed agent-swarm-v1 graph and execution happens only through sandbox-run after the user applies the proposal.",
|
|
74
92
|
};
|
|
75
93
|
|
|
76
94
|
/**
|
|
@@ -85,6 +103,10 @@ const INTENT_DESCRIPTIONS = {
|
|
|
85
103
|
* `create_object` when the prompt mentions both "object" and "API".
|
|
86
104
|
*/
|
|
87
105
|
const INTENT_HEURISTIC_PATTERNS = [
|
|
106
|
+
{ intent: "swarm", patterns: [
|
|
107
|
+
/\b(swarm|sub-?agents?|multi-?agent|agent\s+team|orchestrat(e|or|ion))\b/i,
|
|
108
|
+
/\b(run|launch|spawn|dispatch)\b.*\b(agents?|workers?)\b.*\b(parallel|swarm|workflow)\b/i,
|
|
109
|
+
]},
|
|
88
110
|
{ intent: "register_api", patterns: [
|
|
89
111
|
/\b(api|endpoint|webhook|integration|connector|oauth|bearer\s+token|auth\s+header)\b/i,
|
|
90
112
|
/\b(register|connect|wire|hook\s*up)\b.*\b(api|endpoint|webhook|service|integration)\b/i,
|
|
@@ -196,6 +218,13 @@ function buildStableSystemPrompt(intent) {
|
|
|
196
218
|
"- Reset invalid axis, filter, group, and sort settings when source changes.",
|
|
197
219
|
"- Mark recomputed values as unsaved unless PATCH succeeds.",
|
|
198
220
|
"",
|
|
221
|
+
"## When proposing agent swarms (swarm.run.propose)",
|
|
222
|
+
"- You may propose swarm.run.propose. You are propose-only. Do not execute. Do not store credentials.",
|
|
223
|
+
"- Describe the swarm objective, agent roles, tasks, tools, maxConcurrency, and outcomeCriteria.",
|
|
224
|
+
"- The server will normalize this into the governed agent-swarm graph; execution happens only through sandbox-run after apply.",
|
|
225
|
+
"- payload shape: { name, description?, objective, agents: [{ id?, role, description?, taskPrompt, tools?, required?, maxTokens?, timeoutMs? }], maxConcurrency?, outcomeCriteria?, runLocality?, agentHost?, adapter? }",
|
|
226
|
+
"- adapter, when set, must be local-agent-host or local-intelligence.",
|
|
227
|
+
"",
|
|
199
228
|
"## Valid proposal types and their target patch field",
|
|
200
229
|
WORKSPACE_HELPER_PROPOSAL_TYPES.map(
|
|
201
230
|
(t) => ` ${t} → ${PROPOSAL_TYPE_TO_PATCH_FIELD[t]}`
|
|
@@ -545,8 +545,8 @@ function deriveWorkspaceWorkflowMetadataItems(workspaceConfig, objectItems) {
|
|
|
545
545
|
const graphEdges = Array.isArray(graph?.edges) ? graph.edges : [];
|
|
546
546
|
const workflowMetadataId = stableId("workflow", objectId, rowName);
|
|
547
547
|
const sandboxMetadataId = stableId("sandbox", objectId, rowName);
|
|
548
|
-
|
|
549
|
-
|
|
548
|
+
const rowAgentHost = safeString(row.agentHost).trim();
|
|
549
|
+
const rowAdapter = safeString(row.adapter).trim();
|
|
550
550
|
const inputSchema = graphNodes.length ? discoverRunInputSchema(graph) : { requiresInput: false, fields: [] };
|
|
551
551
|
const inputFields = Array.isArray(inputSchema?.fields) ? inputSchema.fields : [];
|
|
552
552
|
|
|
@@ -560,8 +560,8 @@ function deriveWorkspaceWorkflowMetadataItems(workspaceConfig, objectItems) {
|
|
|
560
560
|
lifecycleStatus: safeString(row.lifecycleStatus).trim() || "draft",
|
|
561
561
|
version: safeString(row.version).trim() || "1",
|
|
562
562
|
sandboxMetadataId,
|
|
563
|
-
agentHost,
|
|
564
|
-
adapter,
|
|
563
|
+
agentHost: rowAgentHost,
|
|
564
|
+
adapter: rowAdapter,
|
|
565
565
|
runLocality: safeString(row.runLocality).trim(),
|
|
566
566
|
nodeCount: graphNodes.length,
|
|
567
567
|
edgeCount: graphEdges.length,
|
|
@@ -578,6 +578,8 @@ function deriveWorkspaceWorkflowMetadataItems(workspaceConfig, objectItems) {
|
|
|
578
578
|
const sourceType = safeString(config.sourceType).trim();
|
|
579
579
|
const sourceId = safeString(config.sourceId).trim();
|
|
580
580
|
const integrationId = safeString(config.integrationId).trim();
|
|
581
|
+
const nodeAgentHost = safeString(config.agentHost || rowAgentHost).trim();
|
|
582
|
+
const nodeAdapter = safeString(config.adapter || rowAdapter).trim();
|
|
581
583
|
const filterClauses = collectFilterClauses({ op: config.filterMode, clauses: config.filters });
|
|
582
584
|
const writesObjectId = safeString(config.writeObjectId || config.targetObjectId).trim();
|
|
583
585
|
const readsObjectId = sourceId || safeString(config.objectId).trim();
|
|
@@ -603,8 +605,8 @@ function deriveWorkspaceWorkflowMetadataItems(workspaceConfig, objectItems) {
|
|
|
603
605
|
inputFieldCount: inputs.length,
|
|
604
606
|
inputFieldIds: inputs.map((field) => field.id),
|
|
605
607
|
sandboxMetadataId,
|
|
606
|
-
agentHost,
|
|
607
|
-
adapter,
|
|
608
|
+
agentHost: nodeAgentHost,
|
|
609
|
+
adapter: nodeAdapter,
|
|
608
610
|
permissions: nodeType === "api-registry-call" ? ["integration:read"] : []
|
|
609
611
|
});
|
|
610
612
|
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace Resolver Proposal V1 — the governed resolver-file lane for the
|
|
3
|
+
* helper, aligned to AWaC topology + the Causation-ITT loop.
|
|
4
|
+
*
|
|
5
|
+
* AWaC boundary: a resolver is a SERVER FILE under
|
|
6
|
+
* lib/adapters/integrations/resolvers/, NOT workspace config. It therefore must
|
|
7
|
+
* NOT travel through the PATCH allowlist (dashboards|widgetTypes|canvas|
|
|
8
|
+
* dataModel). It is its own proposal lane with affectedField "server-file",
|
|
9
|
+
* gated by the same filesystem/read-only persistence rule, and it emits a
|
|
10
|
+
* receipt — so "state → eligibility → guidance → action → evidence" closes.
|
|
11
|
+
*
|
|
12
|
+
* This module is PURE (build/validate/generate). The actual confined fs write
|
|
13
|
+
* lives in lib/server-resolver-write.js (server-only). Secret-safe: the
|
|
14
|
+
* generated resolver reads its secret from the server env at run time via the
|
|
15
|
+
* authRef candidate keys — never an inlined value.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const RESOLVER_PROPOSAL_TYPE = "resolver.create";
|
|
19
|
+
const RESOLVER_AFFECTED_FIELD = "server-file";
|
|
20
|
+
const RESOLVER_DIR = "lib/adapters/integrations/resolvers";
|
|
21
|
+
|
|
22
|
+
function clean(value) {
|
|
23
|
+
return String(value == null ? "" : value).trim();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function slugify(value, fallback = "resolver") {
|
|
27
|
+
const slug = clean(value)
|
|
28
|
+
.toLowerCase()
|
|
29
|
+
.replace(/\.js$/, "")
|
|
30
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
31
|
+
.replace(/-+/g, "-")
|
|
32
|
+
.replace(/^-|-$/g, "")
|
|
33
|
+
.slice(0, 64);
|
|
34
|
+
return slug || fallback;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function envCandidates(ref) {
|
|
38
|
+
const token = clean(ref).replace(/[^a-z0-9]+/gi, "_").replace(/^_+|_+$/g, "").toUpperCase();
|
|
39
|
+
if (!token) return [];
|
|
40
|
+
return Array.from(new Set([token, `${token}_API_KEY`, `${token}_TOKEN`]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Confine a resolver filename to the resolver dir. Mirrors the register-resolver
|
|
45
|
+
* route's traversal guard so the studio previews exactly what the write accepts.
|
|
46
|
+
*/
|
|
47
|
+
function resolveResolverFilePath(name) {
|
|
48
|
+
const base = slugify(name, "resolver");
|
|
49
|
+
if (base.includes("/") || base.includes("..")) {
|
|
50
|
+
return { ok: false, path: null, filename: null, dir: RESOLVER_DIR, error: "resolver filename must not contain path segments" };
|
|
51
|
+
}
|
|
52
|
+
const filename = `${base}.js`;
|
|
53
|
+
return { ok: true, path: `${RESOLVER_DIR}/${filename}`, filename, dir: RESOLVER_DIR, error: null };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function safeJsString(value) {
|
|
57
|
+
return JSON.stringify(clean(value));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Generate a concrete, runnable resolver module for a specific API. baseUrl /
|
|
62
|
+
* endpoint / rootPath / idField are baked in from the registry row + the
|
|
63
|
+
* response profile, so the file works for THIS API. The secret is read from the
|
|
64
|
+
* server env via the authRef candidate keys at run time — never inlined.
|
|
65
|
+
*/
|
|
66
|
+
function generateResolverCode({ integrationId, baseUrl, endpoint, method, authRef, headerName, prefix, rootPath, idField, entityType }) {
|
|
67
|
+
const id = slugify(integrationId, "integration");
|
|
68
|
+
const url = `${clean(baseUrl).replace(/\/+$/, "")}/${clean(endpoint).replace(/^\/+/, "")}`.replace(/\/$/, "") || clean(baseUrl) || clean(endpoint);
|
|
69
|
+
const m = (clean(method).toUpperCase() || "GET");
|
|
70
|
+
const candidates = envCandidates(authRef);
|
|
71
|
+
const header = clean(headerName) || "authorization";
|
|
72
|
+
const pfx = clean(prefix);
|
|
73
|
+
const root = clean(rootPath);
|
|
74
|
+
const idf = clean(idField) || "id";
|
|
75
|
+
const ent = clean(entityType) || "records";
|
|
76
|
+
|
|
77
|
+
return `// Resolver for "${id}" — generated by the governed helper resolver studio.
|
|
78
|
+
// Server file. Reads its secret from the server env at run time (candidates:
|
|
79
|
+
// ${candidates.join(", ") || "none"}); never hard-code a secret here.
|
|
80
|
+
import { registerSourceResolver } from "../source-resolver-registry.js";
|
|
81
|
+
|
|
82
|
+
const ENV_CANDIDATES = ${JSON.stringify(candidates)};
|
|
83
|
+
function readSecret() {
|
|
84
|
+
for (const key of ENV_CANDIDATES) {
|
|
85
|
+
if (process.env[key]) return process.env[key];
|
|
86
|
+
}
|
|
87
|
+
return "";
|
|
88
|
+
}
|
|
89
|
+
function valueAtPath(obj, path) {
|
|
90
|
+
if (!path) return obj;
|
|
91
|
+
return String(path).split(".").filter(Boolean).reduce((acc, part) => (acc == null ? acc : acc[part]), obj);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
registerSourceResolver({
|
|
95
|
+
integrationId: ${safeJsString(id)},
|
|
96
|
+
label: ${safeJsString(`${id} (${ent})`)},
|
|
97
|
+
// fetchRecords runs server-side. Secret stays in the server env.
|
|
98
|
+
fetchRecords: async () => {
|
|
99
|
+
const secret = readSecret();
|
|
100
|
+
const res = await fetch(${safeJsString(url)}, {
|
|
101
|
+
method: ${safeJsString(m)},
|
|
102
|
+
headers: {
|
|
103
|
+
accept: "application/json",
|
|
104
|
+
...(secret ? { ${safeJsString(header)}: ${pfx ? `\`${pfx} \${secret}\`` : "secret"} } : {}),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
const raw = res.headers.get("content-type")?.includes("application/json") ? await res.json() : await res.text();
|
|
108
|
+
const extracted = valueAtPath(raw, ${safeJsString(root)});
|
|
109
|
+
const items = Array.isArray(extracted) ? extracted : (Array.isArray(raw) ? raw : []);
|
|
110
|
+
return items.map((item, index) => ({
|
|
111
|
+
id: item && item[${safeJsString(idf)}] != null ? String(item[${safeJsString(idf)}]) : String(index),
|
|
112
|
+
...item,
|
|
113
|
+
}));
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
`;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build an inert resolver.create proposal. Carries the target path, the
|
|
121
|
+
* generated code, security notes, and the resolver's input/output contract.
|
|
122
|
+
* Nothing is written until the apply lane writes the file (gated).
|
|
123
|
+
*/
|
|
124
|
+
function buildResolverProposal(input = {}) {
|
|
125
|
+
const integrationId = slugify(input.integrationId, "integration");
|
|
126
|
+
const target = resolveResolverFilePath(integrationId);
|
|
127
|
+
const code = generateResolverCode({
|
|
128
|
+
integrationId,
|
|
129
|
+
baseUrl: input.baseUrl,
|
|
130
|
+
endpoint: input.endpoint,
|
|
131
|
+
method: input.method,
|
|
132
|
+
authRef: input.authRef,
|
|
133
|
+
headerName: input.headerName,
|
|
134
|
+
prefix: input.prefix,
|
|
135
|
+
rootPath: input.rootPath,
|
|
136
|
+
idField: input.idField,
|
|
137
|
+
entityType: input.entityType,
|
|
138
|
+
});
|
|
139
|
+
return {
|
|
140
|
+
type: RESOLVER_PROPOSAL_TYPE,
|
|
141
|
+
affectedField: RESOLVER_AFFECTED_FIELD,
|
|
142
|
+
rationale: `Generate a server resolver that shapes ${integrationId}'s response${clean(input.rootPath) ? ` at "${clean(input.rootPath)}"` : ""} into governed rows.`,
|
|
143
|
+
confidence: 0.85,
|
|
144
|
+
target,
|
|
145
|
+
language: "javascript",
|
|
146
|
+
code,
|
|
147
|
+
security: [
|
|
148
|
+
"Server file — only written in filesystem mode (read-only runtimes get a 409 + guidance).",
|
|
149
|
+
"Reads its secret from the server env at run time; never inlines a value.",
|
|
150
|
+
`Path confined to ${RESOLVER_DIR}.`,
|
|
151
|
+
],
|
|
152
|
+
contract: {
|
|
153
|
+
inputs: ["registry baseUrl/endpoint", `authRef env (${envCandidates(input.authRef).join(", ") || "none"})`],
|
|
154
|
+
output: `Record[] with a stable id (from "${clean(input.idField) || "id"}").`,
|
|
155
|
+
},
|
|
156
|
+
payload: {
|
|
157
|
+
integrationId,
|
|
158
|
+
rootPath: clean(input.rootPath),
|
|
159
|
+
entityType: clean(input.entityType) || "records",
|
|
160
|
+
filename: target.filename,
|
|
161
|
+
},
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Validate a resolver.create proposal before the apply lane writes it.
|
|
167
|
+
* Returns { ok, error }.
|
|
168
|
+
*/
|
|
169
|
+
function validateResolverProposal(proposal) {
|
|
170
|
+
if (!proposal || proposal.type !== RESOLVER_PROPOSAL_TYPE) {
|
|
171
|
+
return { ok: false, error: "not a resolver.create proposal" };
|
|
172
|
+
}
|
|
173
|
+
if (proposal.affectedField !== RESOLVER_AFFECTED_FIELD) {
|
|
174
|
+
return { ok: false, error: `resolver.create affectedField must be "${RESOLVER_AFFECTED_FIELD}"` };
|
|
175
|
+
}
|
|
176
|
+
const target = proposal.target || resolveResolverFilePath(proposal.payload?.integrationId);
|
|
177
|
+
if (!target || !target.ok) {
|
|
178
|
+
return { ok: false, error: target?.error || "invalid resolver target path" };
|
|
179
|
+
}
|
|
180
|
+
const code = clean(proposal.code);
|
|
181
|
+
if (!code) return { ok: false, error: "resolver code is empty" };
|
|
182
|
+
if (!code.includes("registerSourceResolver")) {
|
|
183
|
+
return { ok: false, error: "resolver code must call registerSourceResolver()" };
|
|
184
|
+
}
|
|
185
|
+
if (!target.filename.endsWith(".js")) {
|
|
186
|
+
return { ok: false, error: "resolver file must have a .js extension" };
|
|
187
|
+
}
|
|
188
|
+
return { ok: true, error: null, target };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export {
|
|
192
|
+
RESOLVER_PROPOSAL_TYPE,
|
|
193
|
+
RESOLVER_AFFECTED_FIELD,
|
|
194
|
+
RESOLVER_DIR,
|
|
195
|
+
resolveResolverFilePath,
|
|
196
|
+
generateResolverCode,
|
|
197
|
+
buildResolverProposal,
|
|
198
|
+
validateResolverProposal,
|
|
199
|
+
envCandidates,
|
|
200
|
+
};
|