@ateam-ai/mcp 0.3.56 → 0.4.0
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/package.json +1 -1
- package/src/tools.js +340 -31
package/package.json
CHANGED
package/src/tools.js
CHANGED
|
@@ -83,6 +83,89 @@ function _resolveDottedField(obj, dottedPath) {
|
|
|
83
83
|
return { parent, leaf: parts[parts.length - 1] };
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
// ─── Protected array fields (v0.4.0 sibling-loss guard) ─────────────
|
|
87
|
+
//
|
|
88
|
+
// Historically, `ateam_patch(target:"solution", updates:{ linked_skills:["only-one"] })`
|
|
89
|
+
// silently REPLACED the whole linked_skills array — wiping every other
|
|
90
|
+
// skill wired into the solution. Same footgun on ui_plugins, handoffs,
|
|
91
|
+
// grants, connectors, etc. The Solution Builder skill hit this in the
|
|
92
|
+
// wild and wiped a tenant's whole solution.
|
|
93
|
+
//
|
|
94
|
+
// From v0.4.0, ateam_patch REFUSES a bare array-replace on any of these
|
|
95
|
+
// fields unless the caller explicitly opts in via one of:
|
|
96
|
+
// updates: { _replace: true, linked_skills: [...] } // object-level
|
|
97
|
+
// updates: { linked_skills: [...], linked_skills_replace: true } // field-level
|
|
98
|
+
// To ADD or REMOVE without opt-in, use the _push / _delete / _update
|
|
99
|
+
// suffix pattern that has always been the correct form.
|
|
100
|
+
const SOLUTION_ARRAY_FIELDS = new Set([
|
|
101
|
+
'linked_skills',
|
|
102
|
+
'ui_plugins',
|
|
103
|
+
'platform_connectors',
|
|
104
|
+
'handoffs',
|
|
105
|
+
'grants',
|
|
106
|
+
'triggers',
|
|
107
|
+
'notification_routes',
|
|
108
|
+
'channels',
|
|
109
|
+
'actor_types',
|
|
110
|
+
'admin_roles',
|
|
111
|
+
'plugins',
|
|
112
|
+
'security_contracts',
|
|
113
|
+
'connectors',
|
|
114
|
+
'skills',
|
|
115
|
+
]);
|
|
116
|
+
|
|
117
|
+
const SKILL_ARRAY_FIELDS = new Set([
|
|
118
|
+
'tools',
|
|
119
|
+
'connectors',
|
|
120
|
+
'handoffs',
|
|
121
|
+
'scenarios',
|
|
122
|
+
'triggers',
|
|
123
|
+
'notification_routes',
|
|
124
|
+
'plugins',
|
|
125
|
+
'bootstrap_tools',
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
// Returns { ok:false, ... } if the write would REPLACE a protected array
|
|
129
|
+
// without an explicit opt-in; returns null if the write is safe to proceed.
|
|
130
|
+
function _guardArrayReplace({ target, key, value, current, updates }) {
|
|
131
|
+
const knownFields = target === 'skill' ? SKILL_ARRAY_FIELDS : SOLUTION_ARRAY_FIELDS;
|
|
132
|
+
if (!knownFields.has(key)) return null;
|
|
133
|
+
if (!Array.isArray(value)) return null;
|
|
134
|
+
const currentArr = Array.isArray(current) ? current : [];
|
|
135
|
+
if (currentArr.length === 0) return null; // nothing to lose
|
|
136
|
+
if (updates && updates._replace === true) return null;
|
|
137
|
+
if (updates && updates[key + '_replace'] === true) return null;
|
|
138
|
+
|
|
139
|
+
// Compute what would be dropped so the error message is actionable.
|
|
140
|
+
const keyOf = (item) => (item && typeof item === 'object') ? (item.id ?? item.name ?? JSON.stringify(item)) : item;
|
|
141
|
+
const newKeys = new Set(value.map(keyOf));
|
|
142
|
+
const dropped = currentArr.map(keyOf).filter(k => !newKeys.has(k));
|
|
143
|
+
const kept = currentArr.map(keyOf).filter(k => newKeys.has(k));
|
|
144
|
+
const wouldAdd = value.map(keyOf).filter(k => !currentArr.map(keyOf).includes(k));
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
ok: false,
|
|
148
|
+
phase: 'patch',
|
|
149
|
+
error:
|
|
150
|
+
`⚠️ REFUSED: bare-array replace on ${target}.${key} would drop ${dropped.length} sibling item(s): ` +
|
|
151
|
+
`[${dropped.slice(0, 8).join(', ')}${dropped.length > 8 ? ', ...' : ''}]. ` +
|
|
152
|
+
`This footgun wiped a whole solution in the wild — v0.4.0 refuses it by default. ` +
|
|
153
|
+
`\n\nWhat to do instead:` +
|
|
154
|
+
`\n • To ADD items: updates: { "${key}_push": ${JSON.stringify(wouldAdd)} }` +
|
|
155
|
+
(dropped.length ? `\n • To REMOVE items: updates: { "${key}_delete": ${JSON.stringify(dropped)} }` : '') +
|
|
156
|
+
`\n • To FULLY REPLACE (rare): updates: { "${key}": [...], "${key}_replace": true }` +
|
|
157
|
+
`\n • To replace many arrays in one call: updates: { _replace: true, "${key}": [...] }`,
|
|
158
|
+
dropped_ids: dropped,
|
|
159
|
+
kept_ids: kept,
|
|
160
|
+
would_add: wouldAdd,
|
|
161
|
+
safe_alternatives: {
|
|
162
|
+
push: { [`${key}_push`]: wouldAdd },
|
|
163
|
+
...(dropped.length && { delete: { [`${key}_delete`]: dropped } }),
|
|
164
|
+
force_replace: { [key]: value, [`${key}_replace`]: true },
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
86
169
|
// ─── Tool definitions ───────────────────────────────────────────────
|
|
87
170
|
|
|
88
171
|
export const tools = [
|
|
@@ -131,15 +214,15 @@ export const tools = [
|
|
|
131
214
|
name: "ateam_get_spec",
|
|
132
215
|
core: true,
|
|
133
216
|
description:
|
|
134
|
-
"Get the A-Team specification — schemas, validation rules, system tools, agent guides, and templates. Start here after bootstrap to understand how to build skills and solutions. Use 'section' to get just one part of the skill spec (much smaller than the full spec). Use 'search' to find specific fields or concepts across the spec.\n\nWhen designing a persona that orchestrates logic via run_python_script (the Python-as-orchestrator pattern), also fetch topic='python_helpers' — that returns the adas.* helper namespace reference. Skills designed without knowing about adas.* produce 5-10x larger / brittler scripts.",
|
|
217
|
+
"Get the A-Team specification — schemas, validation rules, system tools, agent guides, and templates. Start here after bootstrap to understand how to build skills and solutions. Use 'section' to get just one part of the skill spec (much smaller than the full spec). Use 'search' to find specific fields or concepts across the spec.\n\nWhen designing a persona that orchestrates logic via run_python_script (the Python-as-orchestrator pattern), also fetch topic='python_helpers' — that returns the adas.* helper namespace reference. Skills designed without knowing about adas.* produce 5-10x larger / brittler scripts.\n\nWhen wiring widgets (UI plugins) into a solution, fetch topic='widgets' — that returns the widget spec (catalog model, how_to_use blocks, opener_call shape, persona phrasing rules, binding semantics) so you can declare `ui_plugins` correctly. For the live catalog of widgets actually available in a deployed tenant, use ateam_get_widget_catalog instead.",
|
|
135
218
|
inputSchema: {
|
|
136
219
|
type: "object",
|
|
137
220
|
properties: {
|
|
138
221
|
topic: {
|
|
139
222
|
type: "string",
|
|
140
|
-
enum: ["overview", "skill", "solution", "enums", "connector-multi-user", "python_helpers"],
|
|
223
|
+
enum: ["overview", "skill", "solution", "enums", "connector-multi-user", "python_helpers", "widgets"],
|
|
141
224
|
description:
|
|
142
|
-
"What to fetch: 'overview' = API overview + endpoints, 'skill' = full skill spec, 'solution' = full solution spec, 'enums' = all enum values, 'connector-multi-user' = multi-user connector guide, 'python_helpers' = adas.* helper namespace for run_python_script orchestration (read this when designing personas that read state → call tools → checkpoint → status; without it, scripts hand-roll JSON parsing and tool delegation = 5-10x larger and brittler).",
|
|
225
|
+
"What to fetch: 'overview' = API overview + endpoints, 'skill' = full skill spec, 'solution' = full solution spec, 'enums' = all enum values, 'connector-multi-user' = multi-user connector guide, 'python_helpers' = adas.* helper namespace for run_python_script orchestration (read this when designing personas that read state → call tools → checkpoint → status; without it, scripts hand-roll JSON parsing and tool delegation = 5-10x larger and brittler), 'widgets' = widget (UI plugin) spec: catalog model, how_to_use block shape (solution.json snippet + opener_call + persona_phrasing + binding_notes), and rules for declaring ui_plugins. Pair with ateam_get_widget_catalog for the live per-tenant inventory.",
|
|
143
226
|
},
|
|
144
227
|
section: {
|
|
145
228
|
type: "string",
|
|
@@ -428,24 +511,27 @@ export const tools = [
|
|
|
428
511
|
core: true,
|
|
429
512
|
description:
|
|
430
513
|
"Surgically update ANY field in a skill or solution definition, redeploy, and optionally re-test — all in one step.\n\n" +
|
|
431
|
-
"
|
|
514
|
+
"⚠️ MERGE-BY-DEFAULT (v0.4.0) — Arrays are protected from silent replace. Bare array writes on solution.linked_skills / ui_plugins / platform_connectors / handoffs / grants / triggers (etc.) and skill.tools / connectors / handoffs / scenarios are REFUSED to prevent sibling loss. Add or remove items with the _push / _delete / _update suffixes; opt into a full-array replace only when you really mean it.\n\n" +
|
|
515
|
+
"OPERATIONS (safe by construction):\n" +
|
|
432
516
|
"1. Scalar (dot notation): { \"problem.statement\": \"new value\", \"role.persona\": \"You are...\" }\n" +
|
|
433
517
|
"2. Deep nested: { \"intents.thresholds.accept\": 0.9, \"policy.escalation.enabled\": true }\n" +
|
|
434
|
-
"3. Array
|
|
435
|
-
"4. Array
|
|
436
|
-
"5. Array
|
|
437
|
-
"6.
|
|
438
|
-
"EXAMPLES:\n" +
|
|
439
|
-
"-
|
|
440
|
-
"-
|
|
518
|
+
"3. Array APPEND: { \"tools_push\": [{ name: \"new_tool\", description: \"...\" }] }\n" +
|
|
519
|
+
"4. Array REMOVE: { \"tools_delete\": [\"tool_name\"] }\n" +
|
|
520
|
+
"5. Array MODIFY-ONE: { \"tools_update\": [{ name: \"existing_tool\", description: \"updated\" }] }\n" +
|
|
521
|
+
"6. Full-array REPLACE (opt-in): { \"linked_skills\": [...], \"linked_skills_replace\": true } — or { _replace: true, ... } to opt every array in this call.\n\n" +
|
|
522
|
+
"SOLUTION-LEVEL EXAMPLES (target='solution'):\n" +
|
|
523
|
+
"- ADD a skill to the solution: updates: { \"linked_skills_push\": [\"my-new-skill\"] } ← NOT { linked_skills: [\"my-new-skill\"] } (that would REFUSE — it drops your other skills)\n" +
|
|
524
|
+
"- REMOVE a skill: updates: { \"linked_skills_delete\": [\"old-skill\"] }\n" +
|
|
525
|
+
"- ADD a UI plugin: updates: { \"ui_plugins_push\": [{ id: \"mcp:conn:panel\", ... }] }\n" +
|
|
526
|
+
"- ADD a handoff: updates: { \"handoffs_push\": [{ id: \"h1\", ... }] }\n\n" +
|
|
527
|
+
"SKILL-LEVEL EXAMPLES (target='skill' + skill_id):\n" +
|
|
528
|
+
"- Change persona: updates: { \"role.persona\": \"You are a friendly assistant\" }\n" +
|
|
529
|
+
"- Append to persona: updates: { \"persona_append\": \"\\n\\nALWAYS respond in 2 sentences.\" }\n" +
|
|
441
530
|
"- Add a guardrail: updates: { \"policy.guardrails.never_push\": [\"Never share passwords\"] }\n" +
|
|
442
|
-
"- Update problem: updates: { \"problem.statement\": \"...\", \"problem.goals\": [\"goal1\"] }\n" +
|
|
443
531
|
"- Add a tool: updates: { \"tools_push\": [{ name: \"conn.tool\", description: \"...\", inputs: [...], output: {...} }] }\n" +
|
|
444
532
|
"- Change intent: updates: { \"intents.supported_update\": [{ id: \"i1\", description: \"new desc\" }] }\n" +
|
|
445
|
-
"-
|
|
446
|
-
"
|
|
447
|
-
" If the skill doesn't exist yet, a default scaffold is created and the updates are applied on top. The skill is automatically added to the solution topology.\n\n" +
|
|
448
|
-
"Use target='skill' + skill_id for skill fields. Use target='solution' for solution-level fields (linked_skills, platform_connectors, ui_plugins).",
|
|
533
|
+
"- CREATE a new skill: target='skill', skill_id='my-new-skill', updates: { \"problem.statement\": \"...\", \"role.persona\": \"...\" } — auto-scaffolded and added to solution topology.\n\n" +
|
|
534
|
+
"PREVIEW BEFORE WRITING: pass dry_run:true to see the diff (arrays_merged, arrays_replaced, dropped_ids, added_ids) without applying. Use this before any destructive-looking edit.",
|
|
449
535
|
inputSchema: {
|
|
450
536
|
type: "object",
|
|
451
537
|
properties: {
|
|
@@ -473,6 +559,10 @@ export const tools = [
|
|
|
473
559
|
type: "string",
|
|
474
560
|
description: "Optional: re-test the skill after patching. Requires skill_id.",
|
|
475
561
|
},
|
|
562
|
+
dry_run: {
|
|
563
|
+
type: "boolean",
|
|
564
|
+
description: "If true, apply the patch in memory and return the diff (arrays_merged, arrays_replaced, dropped_ids, added_ids, would_write_bytes) WITHOUT writing to GitHub or redeploying. Preview a change before committing to it.",
|
|
565
|
+
},
|
|
476
566
|
},
|
|
477
567
|
required: ["solution_id", "target", "updates"],
|
|
478
568
|
},
|
|
@@ -516,7 +606,9 @@ export const tools = [
|
|
|
516
606
|
name: "ateam_delete_solution",
|
|
517
607
|
core: true,
|
|
518
608
|
description:
|
|
519
|
-
"
|
|
609
|
+
"⚠️ IRREVERSIBLE — kills Mongo state, running MCP processes, and Builder FS for the whole solution and every skill. " +
|
|
610
|
+
"REQUIRES `confirm:true` AND `confirm_solution_id` echoing the solution id you're destroying (defeats typos and hallucinated ids). " +
|
|
611
|
+
"RECOVERY: the GitHub repo is untouched; `ateam_github_pull` rebuilds the solution from `main`. Prefer that over re-deploying from memory.",
|
|
520
612
|
inputSchema: {
|
|
521
613
|
type: "object",
|
|
522
614
|
properties: {
|
|
@@ -524,15 +616,24 @@ export const tools = [
|
|
|
524
616
|
type: "string",
|
|
525
617
|
description: "The solution ID to delete",
|
|
526
618
|
},
|
|
619
|
+
confirm: {
|
|
620
|
+
type: "boolean",
|
|
621
|
+
description: "REQUIRED. Must be exactly true. A missing/false value refuses the call with a recovery hint.",
|
|
622
|
+
},
|
|
623
|
+
confirm_solution_id: {
|
|
624
|
+
type: "string",
|
|
625
|
+
description: "REQUIRED. Must exactly equal `solution_id`. This defeats typos and hallucinated ids — you can't wipe a solution you couldn't spell.",
|
|
626
|
+
},
|
|
527
627
|
},
|
|
528
|
-
required: ["solution_id"],
|
|
628
|
+
required: ["solution_id", "confirm", "confirm_solution_id"],
|
|
529
629
|
},
|
|
530
630
|
},
|
|
531
631
|
{
|
|
532
632
|
name: "ateam_delete_skill",
|
|
533
633
|
core: true,
|
|
534
634
|
description:
|
|
535
|
-
"
|
|
635
|
+
"⚠️ IRREVERSIBLE in Core + Builder FS — kills the running MCP process, unregisters from skill registry, deletes the Mongo record, drops from solution.skills[] and solution.linked_skills, and removes the skill's files from Builder FS. " +
|
|
636
|
+
"REQUIRES `confirm:true`. RECOVERY: the skill still lives in GitHub — `ateam_github_pull` rebuilds the whole solution (no per-skill restore path).",
|
|
536
637
|
inputSchema: {
|
|
537
638
|
type: "object",
|
|
538
639
|
properties: {
|
|
@@ -544,15 +645,22 @@ export const tools = [
|
|
|
544
645
|
type: "string",
|
|
545
646
|
description: "The skill ID to remove (e.g. 'linkedin-agent')",
|
|
546
647
|
},
|
|
648
|
+
confirm: {
|
|
649
|
+
type: "boolean",
|
|
650
|
+
description: "REQUIRED. Must be exactly true. A missing/false value refuses the call with a recovery hint.",
|
|
651
|
+
},
|
|
547
652
|
},
|
|
548
|
-
required: ["solution_id", "skill_id"],
|
|
653
|
+
required: ["solution_id", "skill_id", "confirm"],
|
|
549
654
|
},
|
|
550
655
|
},
|
|
551
656
|
{
|
|
552
657
|
name: "ateam_delete_connector",
|
|
553
658
|
core: true,
|
|
554
659
|
description:
|
|
555
|
-
"
|
|
660
|
+
"⚠️ CASCADING — any skill whose engine.bootstrap_tools or tools[] name a tool from this connector will FAIL its next execution. " +
|
|
661
|
+
"Stops and deletes the connector from A-Team Core; drops references from the solution definition (grants, platform_connectors, ui_plugins ids starting `mcp:<connector-id>:*`) and skill definitions (connectors array); cleans up mcp-store files. " +
|
|
662
|
+
"GitHub source is preserved — a follow-up `ateam_build_and_run(github:true)` can resurrect. " +
|
|
663
|
+
"REQUIRES `confirm:true`.",
|
|
556
664
|
inputSchema: {
|
|
557
665
|
type: "object",
|
|
558
666
|
properties: {
|
|
@@ -564,8 +672,12 @@ export const tools = [
|
|
|
564
672
|
type: "string",
|
|
565
673
|
description: "The connector ID to remove (e.g. 'device-mock-mcp')",
|
|
566
674
|
},
|
|
675
|
+
confirm: {
|
|
676
|
+
type: "boolean",
|
|
677
|
+
description: "REQUIRED. Must be exactly true. A missing/false value refuses the call with a recovery hint.",
|
|
678
|
+
},
|
|
567
679
|
},
|
|
568
|
-
required: ["solution_id", "connector_id"],
|
|
680
|
+
required: ["solution_id", "connector_id", "confirm"],
|
|
569
681
|
},
|
|
570
682
|
},
|
|
571
683
|
|
|
@@ -993,6 +1105,45 @@ export const tools = [
|
|
|
993
1105
|
required: ["job_id"],
|
|
994
1106
|
},
|
|
995
1107
|
},
|
|
1108
|
+
{
|
|
1109
|
+
name: "ateam_get_widget_catalog",
|
|
1110
|
+
core: true,
|
|
1111
|
+
description:
|
|
1112
|
+
"Get the live catalog of widgets (UI plugins) available in this tenant's solution. Returns platform-bundled + solution-bundled + skill-declared widgets, each with a paste-ready how_to_use block (solution.json snippet + opener_call + persona_phrasing + binding_notes).\n\n" +
|
|
1113
|
+
"Use this when wiring widgets into a skill or solution — the how_to_use block is designed to be copied verbatim into the solution.json ui_plugins[] entry and into the persona's opener phrasing, so you don't have to hand-roll either. The catalog reflects what is actually deployed in the tenant right now, not the abstract spec (for the spec itself, use ateam_get_spec topic='widgets').\n\n" +
|
|
1114
|
+
"Origins:\n" +
|
|
1115
|
+
" • 'platform' = widgets bundled with the platform (always available).\n" +
|
|
1116
|
+
" • 'solution' = widgets bundled with this tenant's solution.\n" +
|
|
1117
|
+
" • 'skill' = widgets declared by a specific skill in the solution.\n\n" +
|
|
1118
|
+
"Auth: forwards your authed api_key to Core (no master-secret involvement). Tenant scope is pinned by the key itself.",
|
|
1119
|
+
inputSchema: {
|
|
1120
|
+
type: "object",
|
|
1121
|
+
properties: {
|
|
1122
|
+
solution_id: {
|
|
1123
|
+
type: "string",
|
|
1124
|
+
description: "Optional. The solution to query. Defaults to the tenant's current solution.",
|
|
1125
|
+
},
|
|
1126
|
+
origin: {
|
|
1127
|
+
type: "string",
|
|
1128
|
+
enum: ["all", "platform", "solution", "skill"],
|
|
1129
|
+
description:
|
|
1130
|
+
"Optional. Filter by widget origin. 'all' (default) returns everything. 'platform' = platform-bundled only. 'solution' = solution-bundled only. 'skill' = skill-declared only.",
|
|
1131
|
+
},
|
|
1132
|
+
include_unused: {
|
|
1133
|
+
type: "boolean",
|
|
1134
|
+
description:
|
|
1135
|
+
"Optional. If true, includes widgets that are available but not currently referenced by any skill or ui_plugins entry. Default false (only widgets actually wired into the solution).",
|
|
1136
|
+
},
|
|
1137
|
+
format: {
|
|
1138
|
+
type: "string",
|
|
1139
|
+
enum: ["summary", "full"],
|
|
1140
|
+
description:
|
|
1141
|
+
"Optional. 'full' (default) returns each widget with its paste-ready how_to_use block (solution.json snippet, opener_call, persona_phrasing, binding_notes). 'summary' returns just id/name/origin/description for a quick overview.",
|
|
1142
|
+
},
|
|
1143
|
+
},
|
|
1144
|
+
required: [],
|
|
1145
|
+
},
|
|
1146
|
+
},
|
|
996
1147
|
{
|
|
997
1148
|
name: "ateam_test_abort",
|
|
998
1149
|
core: true,
|
|
@@ -1512,6 +1663,7 @@ const SPEC_PATHS = {
|
|
|
1512
1663
|
enums: "/spec/enums",
|
|
1513
1664
|
"connector-multi-user": "/spec/multi-user-connector",
|
|
1514
1665
|
python_helpers: "/spec/python_helpers",
|
|
1666
|
+
widgets: "/spec/widgets",
|
|
1515
1667
|
};
|
|
1516
1668
|
|
|
1517
1669
|
const EXAMPLE_PATHS = {
|
|
@@ -1554,6 +1706,7 @@ const TENANT_TOOLS = new Set([
|
|
|
1554
1706
|
"ateam_test_status",
|
|
1555
1707
|
"ateam_test_abort",
|
|
1556
1708
|
"ateam_get_chain",
|
|
1709
|
+
"ateam_get_widget_catalog",
|
|
1557
1710
|
"ateam_get_connector_source",
|
|
1558
1711
|
"ateam_get_metrics",
|
|
1559
1712
|
"ateam_diff",
|
|
@@ -2494,9 +2647,10 @@ const handlers = {
|
|
|
2494
2647
|
// Updates → Redeploys → Optionally tests
|
|
2495
2648
|
// One call replaces: ateam_update + ateam_redeploy
|
|
2496
2649
|
|
|
2497
|
-
ateam_patch: async ({ solution_id, target, skill_id, updates, test_message }, sid) => {
|
|
2650
|
+
ateam_patch: async ({ solution_id, target, skill_id, updates, test_message, dry_run }, sid) => {
|
|
2498
2651
|
const phases = [];
|
|
2499
2652
|
let isNewSkill = false;
|
|
2653
|
+
const _diff = { arrays_merged: [], arrays_replaced: [], scalars_changed: [], sections_replaced: [] };
|
|
2500
2654
|
|
|
2501
2655
|
// GitHub-first patch: read from GitHub → apply patch → write back → redeploy
|
|
2502
2656
|
// This ensures GitHub stays the single source of truth.
|
|
@@ -2528,7 +2682,7 @@ const handlers = {
|
|
|
2528
2682
|
role: { name: "", persona: "", goals: [], limitations: [], communication_style: { tone: "professional", verbosity: "concise" } },
|
|
2529
2683
|
intents: { supported: [], thresholds: { accept: 0.8, clarify: 0.5, reject: 0.5 }, out_of_domain: { action: "redirect", message: "" } },
|
|
2530
2684
|
tools: [],
|
|
2531
|
-
policy: { guardrails: { never: [], always: [] }, approvals: [], workflows: [], escalation: { enabled: false, conditions: [], target: "" } },
|
|
2685
|
+
policy: { access: { requires_roles: [] }, guardrails: { never: [], always: [] }, approvals: [], workflows: [], escalation: { enabled: false, conditions: [], target: "" } },
|
|
2532
2686
|
engine: { rv2: { max_iterations: 10, iteration_timeout_ms: 120000, allow_parallel_tools: false, on_max_iterations: "ask_user" }, hlr: { enabled: true, critic: { enabled: true, check_interval: 3, strictness: "medium" }, reflection: { enabled: true, depth: "shallow" }, replanning: { enabled: true, max_replans: 3 } }, autonomy: { level: "supervised" }, finalization_gate: { enabled: true, max_retries: 2 } },
|
|
2533
2687
|
access_policy: { rules: [{ tools: ["*"], effect: "allow" }] },
|
|
2534
2688
|
grant_mappings: [],
|
|
@@ -2615,9 +2769,30 @@ const handlers = {
|
|
|
2615
2769
|
else arr.push(upd);
|
|
2616
2770
|
}
|
|
2617
2771
|
parent[leaf] = arr;
|
|
2772
|
+
} else if (key === "_replace" || key.endsWith("_replace")) {
|
|
2773
|
+
// Escape-hatch flags handled by the guard — skip them here so they
|
|
2774
|
+
// don't get written into the patched object as literal fields.
|
|
2775
|
+
continue;
|
|
2618
2776
|
} else if (key.includes(".")) {
|
|
2619
2777
|
// Dot notation: "role.persona", "intents.thresholds.accept"
|
|
2620
2778
|
const parts = key.split(".");
|
|
2779
|
+
// Sibling-loss guard: if the leaf resolves to an existing non-empty
|
|
2780
|
+
// array and the incoming value is also an array, refuse the replace
|
|
2781
|
+
// unless the caller opted in. (Dot-notation is how many agents
|
|
2782
|
+
// accidentally hit this — e.g. updates:{ "linked_skills": ["one"] }
|
|
2783
|
+
// on target='solution'.)
|
|
2784
|
+
const leafKey = parts[parts.length - 1];
|
|
2785
|
+
let cursor = patched;
|
|
2786
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
2787
|
+
if (!cursor || typeof cursor[parts[i]] !== 'object') { cursor = null; break; }
|
|
2788
|
+
cursor = cursor[parts[i]];
|
|
2789
|
+
}
|
|
2790
|
+
const currentLeaf = cursor && Object.prototype.hasOwnProperty.call(cursor, leafKey) ? cursor[leafKey] : undefined;
|
|
2791
|
+
const guardErr = _guardArrayReplace({ target, key: leafKey, value, current: currentLeaf, updates });
|
|
2792
|
+
if (guardErr) return guardErr;
|
|
2793
|
+
if (Array.isArray(value) && Array.isArray(currentLeaf)) _diff.arrays_replaced.push(key);
|
|
2794
|
+
else if (typeof value === 'object' && value !== null && !Array.isArray(value)) _diff.sections_replaced.push(key);
|
|
2795
|
+
else _diff.scalars_changed.push(key);
|
|
2621
2796
|
let obj = patched;
|
|
2622
2797
|
for (let i = 0; i < parts.length - 1; i++) {
|
|
2623
2798
|
if (!obj[parts[i]] || typeof obj[parts[i]] !== "object") obj[parts[i]] = {};
|
|
@@ -2625,7 +2800,14 @@ const handlers = {
|
|
|
2625
2800
|
}
|
|
2626
2801
|
obj[parts[parts.length - 1]] = value;
|
|
2627
2802
|
} else {
|
|
2628
|
-
// Direct field replacement
|
|
2803
|
+
// Direct top-level field replacement. Sibling-loss guard: if this
|
|
2804
|
+
// names a known array field and would drop items, refuse unless the
|
|
2805
|
+
// caller passed _replace:true (object-level) or <field>_replace:true.
|
|
2806
|
+
const guardErr = _guardArrayReplace({ target, key, value, current: patched[key], updates });
|
|
2807
|
+
if (guardErr) return guardErr;
|
|
2808
|
+
if (Array.isArray(value) && Array.isArray(patched[key])) _diff.arrays_replaced.push(key);
|
|
2809
|
+
else if (typeof value === 'object' && value !== null && !Array.isArray(value)) _diff.sections_replaced.push(key);
|
|
2810
|
+
else _diff.scalars_changed.push(key);
|
|
2629
2811
|
patched[key] = value;
|
|
2630
2812
|
}
|
|
2631
2813
|
}
|
|
@@ -2634,6 +2816,23 @@ const handlers = {
|
|
|
2634
2816
|
return { ok: false, phase: "patch", error: `Failed to apply patch: ${err.message}` };
|
|
2635
2817
|
}
|
|
2636
2818
|
|
|
2819
|
+
// Dry-run: return diff + would-be after-state without writing to GitHub
|
|
2820
|
+
// or redeploying. Lets an agent preview any destructive-looking edit.
|
|
2821
|
+
if (dry_run) {
|
|
2822
|
+
return {
|
|
2823
|
+
ok: true,
|
|
2824
|
+
dry_run: true,
|
|
2825
|
+
target,
|
|
2826
|
+
solution_id,
|
|
2827
|
+
skill_id,
|
|
2828
|
+
phases,
|
|
2829
|
+
_diff,
|
|
2830
|
+
after_state: patched,
|
|
2831
|
+
would_write_bytes: JSON.stringify(patched, null, 2).length,
|
|
2832
|
+
hint: "No changes applied. Remove dry_run:true to commit + redeploy.",
|
|
2833
|
+
};
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2637
2836
|
// Phase 3: Write patched version back to GitHub
|
|
2638
2837
|
try {
|
|
2639
2838
|
const patchKeys = Object.keys(updates || {});
|
|
@@ -3097,6 +3296,84 @@ const handlers = {
|
|
|
3097
3296
|
return data;
|
|
3098
3297
|
},
|
|
3099
3298
|
|
|
3299
|
+
ateam_get_widget_catalog: async ({ origin, format }, sid) => {
|
|
3300
|
+
// Wraps Core's existing GET /api/ui-plugins (merged tenant plugin list)
|
|
3301
|
+
// and enriches each entry with the documentation/how-to-use layer.
|
|
3302
|
+
// Filtering by origin and the summary/full format projection happen
|
|
3303
|
+
// client-side here — Core just returns the raw merged plugins[].
|
|
3304
|
+
const creds = getCredentials(sid);
|
|
3305
|
+
const apiKey = creds?.apiKey;
|
|
3306
|
+
if (!apiKey) throw new Error("No api_key in session — call ateam_auth(api_key) first.");
|
|
3307
|
+
const coreUrl = process.env.ADAS_CORE_URL || "http://adas-backend:4000";
|
|
3308
|
+
const res = await fetch(`${coreUrl}/api/ui-plugins`, {
|
|
3309
|
+
method: "GET",
|
|
3310
|
+
headers: { "x-api-key": apiKey, "X-ADAS-SERVICE": "ateam-mcp.get_widget_catalog" },
|
|
3311
|
+
signal: AbortSignal.timeout(15_000),
|
|
3312
|
+
});
|
|
3313
|
+
const text = await res.text();
|
|
3314
|
+
let data;
|
|
3315
|
+
try { data = JSON.parse(text); } catch { data = { ok: false, error: text.slice(0, 400) }; }
|
|
3316
|
+
if (!res.ok) {
|
|
3317
|
+
throw new Error(`Core /api/ui-plugins returned ${res.status}: ${data.error || JSON.stringify(data).slice(0, 200)}`);
|
|
3318
|
+
}
|
|
3319
|
+
|
|
3320
|
+
// Project each plugin into the catalog shape with how_to_use guidance.
|
|
3321
|
+
const plugins = Array.isArray(data?.plugins) ? data.plugins : [];
|
|
3322
|
+
const wantSummary = format === "summary";
|
|
3323
|
+
const filterOrigin = origin && origin !== "all" ? origin : null;
|
|
3324
|
+
|
|
3325
|
+
const widgets = plugins.map((p) => {
|
|
3326
|
+
const id = p?.id || "";
|
|
3327
|
+
const shortId = id.split(":").pop() || id;
|
|
3328
|
+
// origin classification: platform vs solution vs skill
|
|
3329
|
+
const src = p?._source || "";
|
|
3330
|
+
const inferredOrigin = src === "mcp_introspection" ? "platform"
|
|
3331
|
+
: src === "skill_declared" ? "skill"
|
|
3332
|
+
: "solution";
|
|
3333
|
+
const opener = Array.isArray(p?.capabilities?.commands) && p.capabilities.commands.length > 0
|
|
3334
|
+
? `ui.${shortId}.${p.capabilities.commands[0].name || "open"}({ /* args per input_schema */ })`
|
|
3335
|
+
: `sys.focusUiPlugin({ plugin_id: "${id}" })`;
|
|
3336
|
+
const entry = {
|
|
3337
|
+
id,
|
|
3338
|
+
name: p?.name,
|
|
3339
|
+
version: p?.version,
|
|
3340
|
+
description: p?.description,
|
|
3341
|
+
type: p?.type || "ui",
|
|
3342
|
+
origin: inferredOrigin,
|
|
3343
|
+
owned_by_connector: p?._connector_id,
|
|
3344
|
+
render: p?.render,
|
|
3345
|
+
surface: p?.surface,
|
|
3346
|
+
capabilities: p?.capabilities,
|
|
3347
|
+
channels: p?.channels,
|
|
3348
|
+
commands: p?.capabilities?.commands || p?.commands || [],
|
|
3349
|
+
uiActions: p?.uiActions,
|
|
3350
|
+
};
|
|
3351
|
+
if (!wantSummary) {
|
|
3352
|
+
entry.how_to_use = {
|
|
3353
|
+
solution_json_snippet: { id, name: p?.name, version: p?.version, render: p?.render },
|
|
3354
|
+
opener_call: opener,
|
|
3355
|
+
persona_phrasing: `When the user wants to view ${(p?.description || p?.name || shortId).toString().toLowerCase()}, call ${opener.split("(")[0]}.`,
|
|
3356
|
+
binding_notes: {
|
|
3357
|
+
commands_input_schemas: (p?.capabilities?.commands || []).map(c => ({ command: c.name, schema: c.input_schema })),
|
|
3358
|
+
deeplink_template: p?.uiActions?.deeplink || null,
|
|
3359
|
+
view_entity_kinds: p?.uiActions?.intents?.view_entity?.entity_kinds || null,
|
|
3360
|
+
host_auto_routes_intents: Object.keys(p?.uiActions?.intents || {}),
|
|
3361
|
+
},
|
|
3362
|
+
};
|
|
3363
|
+
}
|
|
3364
|
+
return entry;
|
|
3365
|
+
});
|
|
3366
|
+
|
|
3367
|
+
const filtered = filterOrigin ? widgets.filter(w => w.origin === filterOrigin) : widgets;
|
|
3368
|
+
const counts = {
|
|
3369
|
+
total: filtered.length,
|
|
3370
|
+
platform: filtered.filter(w => w.origin === "platform").length,
|
|
3371
|
+
solution: filtered.filter(w => w.origin === "solution").length,
|
|
3372
|
+
skill: filtered.filter(w => w.origin === "skill").length,
|
|
3373
|
+
};
|
|
3374
|
+
return { ok: true, generated_at: new Date().toISOString(), counts, widgets: filtered };
|
|
3375
|
+
},
|
|
3376
|
+
|
|
3100
3377
|
ateam_test_abort: async ({ solution_id, skill_id, job_id }, sid) =>
|
|
3101
3378
|
del(`/deploy/solutions/${solution_id}/skills/${skill_id}/test/${job_id}`, sid),
|
|
3102
3379
|
|
|
@@ -3245,14 +3522,46 @@ const handlers = {
|
|
|
3245
3522
|
ateam_github_list_versions: async ({ solution_id }, sid) =>
|
|
3246
3523
|
get(`/deploy/solutions/${solution_id}/versions/dev`, sid),
|
|
3247
3524
|
|
|
3248
|
-
ateam_delete_solution: async ({ solution_id }, sid) =>
|
|
3249
|
-
|
|
3525
|
+
ateam_delete_solution: async ({ solution_id, confirm, confirm_solution_id }, sid) => {
|
|
3526
|
+
if (confirm !== true) {
|
|
3527
|
+
return {
|
|
3528
|
+
ok: false,
|
|
3529
|
+
error: "⚠️ REFUSED: ateam_delete_solution requires confirm:true. This is irreversible in Core + Builder FS. GitHub source is preserved — ateam_github_pull rebuilds from `main` if you already deleted by mistake.",
|
|
3530
|
+
recovery: "ateam_github_pull(solution_id, ref:'main')",
|
|
3531
|
+
};
|
|
3532
|
+
}
|
|
3533
|
+
if (confirm_solution_id !== solution_id) {
|
|
3534
|
+
return {
|
|
3535
|
+
ok: false,
|
|
3536
|
+
error: `⚠️ REFUSED: confirm_solution_id must exactly equal solution_id. Got confirm_solution_id="${confirm_solution_id}" but solution_id="${solution_id}". This check defeats typos and hallucinated ids — you should not be able to wipe a solution whose id you can't spell correctly.`,
|
|
3537
|
+
expected: solution_id,
|
|
3538
|
+
received: confirm_solution_id,
|
|
3539
|
+
};
|
|
3540
|
+
}
|
|
3541
|
+
return del(`/deploy/solutions/${solution_id}`, sid);
|
|
3542
|
+
},
|
|
3250
3543
|
|
|
3251
|
-
ateam_delete_skill: async ({ solution_id, skill_id }, sid) =>
|
|
3252
|
-
|
|
3544
|
+
ateam_delete_skill: async ({ solution_id, skill_id, confirm }, sid) => {
|
|
3545
|
+
if (confirm !== true) {
|
|
3546
|
+
return {
|
|
3547
|
+
ok: false,
|
|
3548
|
+
error: `⚠️ REFUSED: ateam_delete_skill requires confirm:true. Kills the running MCP process and deletes the skill from Core + Builder FS. GitHub source is preserved — ateam_github_pull rebuilds the whole solution.`,
|
|
3549
|
+
recovery: "ateam_github_pull(solution_id, ref:'main') — no per-skill restore path",
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
return del(`/deploy/solutions/${solution_id}/skills/${skill_id}`, sid);
|
|
3553
|
+
},
|
|
3253
3554
|
|
|
3254
|
-
ateam_delete_connector: async ({ solution_id, connector_id }, sid) =>
|
|
3255
|
-
|
|
3555
|
+
ateam_delete_connector: async ({ solution_id, connector_id, confirm }, sid) => {
|
|
3556
|
+
if (confirm !== true) {
|
|
3557
|
+
return {
|
|
3558
|
+
ok: false,
|
|
3559
|
+
error: `⚠️ REFUSED: ateam_delete_connector requires confirm:true. Cascading — any skill wired to this connector's tools will fail its next execution. GitHub source is preserved.`,
|
|
3560
|
+
recovery: "ateam_build_and_run(solution_id, github:true) can resurrect from GitHub",
|
|
3561
|
+
};
|
|
3562
|
+
}
|
|
3563
|
+
return del(`/deploy/solutions/${solution_id}/connectors/${connector_id}`, sid);
|
|
3564
|
+
},
|
|
3256
3565
|
|
|
3257
3566
|
ateam_upload_connector: async ({ solution_id, connector_id, github, files, ref, replace }, sid) =>
|
|
3258
3567
|
post(
|