@descix/cli 1.0.1 → 1.0.4
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/LICENSE.md +671 -0
- package/README.md +43 -4
- package/agent-assets/guides/README.md +2 -2
- package/agent-assets/guides/dynamic-site-pattern.md +29 -14
- package/agent-assets/guides/knowledge-only-pattern.md +5 -5
- package/agent-assets/guides/microservice-pattern.md +48 -4
- package/agent-assets/guides/static-site-pattern.md +8 -0
- package/agent-assets/instructions/app-asset-validation.md +5 -5
- package/agent-assets/instructions/onboarding-flow.md +4 -4
- package/agent-assets/instructions/standard-folder-structure.md +20 -11
- package/agent-assets/reference/app-creation.md +42 -20
- package/agent-assets/reference/architecture.md +24 -22
- package/agent-assets/reference/local-dev.md +230 -67
- package/agent-assets/reference/mcp-integration.md +21 -15
- package/agent-assets/reference/platform-api-consumer.md +244 -0
- package/agent-assets/reference/workspace-config.md +7 -7
- package/agent-assets/rules/descix_mcp.template.mdc +8 -8
- package/bin/descix.js +2559 -1435
- package/bin/kb-deny-lint.mjs +91 -0
- package/bin/mcp-server.js +175 -91
- package/lib/agent-files.js +37 -25
- package/lib/api-client.js +223 -56
- package/lib/auth-guard.js +13 -3
- package/lib/chat-session.js +187 -0
- package/lib/cli-version.js +59 -0
- package/lib/commands/airdrop.js +251 -0
- package/lib/commands/auth.js +254 -72
- package/lib/commands/briefer/errors.js +88 -0
- package/lib/commands/briefer/index.js +319 -0
- package/lib/commands/briefer/sources/canonical-sources.js +129 -0
- package/lib/commands/briefer/sources/entitlements.js +220 -0
- package/lib/commands/briefer/sources/environments.js +205 -0
- package/lib/commands/briefer/sources/identifiers.js +214 -0
- package/lib/commands/briefer/sources/microservice-deploy.js +267 -0
- package/lib/commands/briefer/sources/routing.js +275 -0
- package/lib/commands/briefer/sources/what-is-not.js +223 -0
- package/lib/commands/briefer/stitcher.js +134 -0
- package/lib/commands/briefer/util/source-reader.js +502 -0
- package/lib/commands/config.js +210 -90
- package/lib/commands/corpus.js +978 -0
- package/lib/commands/credits.js +122 -0
- package/lib/commands/health.js +627 -0
- package/lib/commands/kb-list-render.js +32 -0
- package/lib/commands/kb.js +190 -413
- package/lib/commands/mcp.js +5 -5
- package/lib/commands/microservice-restart.js +359 -0
- package/lib/commands/model-config.js +313 -0
- package/lib/commands/retired-kb-sync.js +134 -0
- package/lib/commands/serve.js +26 -1
- package/lib/commands/status.js +23 -51
- package/lib/commands/update.js +36 -107
- package/lib/core/Chunker.js +491 -17
- package/lib/core/CorpusDenyLint.js +326 -0
- package/lib/core/CorpusWalker.js +357 -0
- package/lib/core/Hydrator.js +75 -29
- package/lib/core/ManifestLoader.js +533 -0
- package/lib/core/SiteWalker.js +218 -0
- package/lib/core/Syncer.js +163 -11
- package/lib/diagnostic-log.js +41 -0
- package/lib/environment-report.js +100 -0
- package/lib/global-config.js +4 -1
- package/lib/oauth-client.js +164 -0
- package/lib/origin.js +143 -0
- package/lib/service-api-client.js +95 -0
- package/lib/wallet-environment.js +49 -0
- package/lib/wallet-file.js +16 -2
- package/lib/wizard/setup.js +10 -9
- package/lib/workspace-config.js +380 -235
- package/lib/workspace-identity.js +92 -0
- package/lib/workspace-utils.js +41 -61
- package/package.json +19 -6
- package/templates/agent-claude.md +157 -9
- package/templates/agent-cline.md +68 -8
- package/templates/agent-copilot.md +68 -8
- package/templates/agent-cursor.md +68 -8
- package/templates/alignment-schema.json +20 -0
- package/templates/cloud-brain-hook.sh +20 -0
- package/templates/drive/README.md +1 -1
- package/templates/drive_templates/README.md +2 -2
- package/templates/scaffolds/README.md +6 -10
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +104 -149
- package/templates/scaffolds/microservice/app.js +51 -10
- package/templates/scaffolds/microservice/app.yaml +2 -2
- package/templates/scaffolds/microservice/defaults-config-dev.json +4 -0
- package/templates/scaffolds/microservice/dev-overrides.example.json +5 -0
- package/templates/scaffolds/microservice/manifest.json +0 -1
- package/templates/scaffolds/microservice/package.json +3 -3
- package/templates/scaffolds/microservice/scripts/register.js +56 -41
- package/templates/scaffolds/microservice/services/mcpClient.js +55 -65
- package/templates/scaffolds/microservice/services/utils.js +20 -238
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +24 -0
- package/lib/commands/app-wizard.js +0 -627
- package/lib/commands/folder.js +0 -291
- package/lib/core/PathContext.js +0 -335
- package/templates/scaffolds/microservice/dev-overrides.json.example +0 -5
- package/templates/scaffolds/site/DeSciXAppSDK.js +0 -62
- package/templates/scaffolds/site/README.md +0 -51
- package/templates/scaffolds/site/app.js +0 -22
- package/templates/scaffolds/site/index.html +0 -42
- package/templates/scaffolds/site/styles.css +0 -123
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* kb-deny-lint.mjs — CLI-side runner for the Tier-P KB-publishing deny-class lint.
|
|
4
|
+
*
|
|
5
|
+
* Thin wrapper around the canonical @descix/cli CorpusDenyLint module (the ONE
|
|
6
|
+
* definition of the pattern set + doc_class taxonomy — this runner re-lists
|
|
7
|
+
* nothing). Used by the `.githooks/check_kb_deny_class.sh` pre-commit gate so a
|
|
8
|
+
* Tier-P manifest cannot be committed with a deny-class source, mirroring the
|
|
9
|
+
* sync-path gate in `corpus.js runCorpusSync`.
|
|
10
|
+
*
|
|
11
|
+
* Usage: node kb-deny-lint.mjs <manifest.json> [<manifest.json> ...]
|
|
12
|
+
* Exit: 0 = clean (Tier-P manifests all pass, or none were Tier-P)
|
|
13
|
+
* 1 = at least one Tier-P manifest has a violation
|
|
14
|
+
* 2 = a manifest could not be read/parsed/validated
|
|
15
|
+
*
|
|
16
|
+
* Only manifests whose `publish_tier === "P"` are gated; others are skipped
|
|
17
|
+
* (current behavior unchanged, matching the sync path).
|
|
18
|
+
*/
|
|
19
|
+
import * as fs from 'fs/promises';
|
|
20
|
+
import * as path from 'path';
|
|
21
|
+
import { loadManifest } from '../lib/core/ManifestLoader.js';
|
|
22
|
+
import { lintManifestForDenyClasses, assertNoViolations } from '../lib/core/CorpusDenyLint.js';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve the workspace root for a manifest by walking up to the nearest
|
|
26
|
+
* ancestor containing `.descix/workspace.json` (the same signal WorkspaceConfig
|
|
27
|
+
* uses). Falls back to the filesystem root's process.cwd() if none is found.
|
|
28
|
+
* Manifest source paths are workspace-relative, so this must match the workspace
|
|
29
|
+
* the sync command would resolve.
|
|
30
|
+
*/
|
|
31
|
+
async function resolveWorkspaceRoot(manifestPath) {
|
|
32
|
+
let dir = path.dirname(path.resolve(manifestPath));
|
|
33
|
+
const { root } = path.parse(dir);
|
|
34
|
+
while (true) {
|
|
35
|
+
try {
|
|
36
|
+
await fs.access(path.join(dir, '.descix', 'workspace.json'));
|
|
37
|
+
return dir;
|
|
38
|
+
} catch { /* keep walking */ }
|
|
39
|
+
if (dir === root) break;
|
|
40
|
+
dir = path.dirname(dir);
|
|
41
|
+
}
|
|
42
|
+
// No workspace.json ancestor — best effort (path/doc_class/license checks still fire).
|
|
43
|
+
return process.cwd();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function main() {
|
|
47
|
+
const manifestPaths = process.argv.slice(2);
|
|
48
|
+
if (manifestPaths.length === 0) {
|
|
49
|
+
console.error('kb-deny-lint: no manifest paths given');
|
|
50
|
+
process.exit(2);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let anyViolation = false;
|
|
54
|
+
let anyLoadError = false;
|
|
55
|
+
|
|
56
|
+
for (const mfPath of manifestPaths) {
|
|
57
|
+
let manifest;
|
|
58
|
+
let workspaceRoot;
|
|
59
|
+
try {
|
|
60
|
+
workspaceRoot = await resolveWorkspaceRoot(mfPath);
|
|
61
|
+
manifest = await loadManifest(mfPath, workspaceRoot);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
// A Tier-P manifest that fails schema validation (e.g. deny-class doc_class,
|
|
64
|
+
// missing doc_class) is itself a hard block — surface it as a violation.
|
|
65
|
+
console.error(`✗ ${mfPath}: ${err.message}`);
|
|
66
|
+
anyLoadError = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (manifest.publish_tier !== 'P') continue; // only Tier-P manifests are gated
|
|
71
|
+
|
|
72
|
+
const report = await lintManifestForDenyClasses(manifest, workspaceRoot);
|
|
73
|
+
for (const ex of report.exemptions) console.log(` ${ex.line}`);
|
|
74
|
+
for (const w of report.warnings) console.log(` ${w.line}`);
|
|
75
|
+
try {
|
|
76
|
+
assertNoViolations(report);
|
|
77
|
+
console.log(`✓ ${mfPath}: Tier-P deny-class lint clean (${report.exemptions.length} exemption(s), ${report.warnings.length} warning(s)).`);
|
|
78
|
+
} catch (err) {
|
|
79
|
+
console.error(`✗ ${err.message}`);
|
|
80
|
+
anyViolation = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (anyLoadError) process.exit(2);
|
|
85
|
+
process.exit(anyViolation ? 1 : 0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
main().catch(err => {
|
|
89
|
+
console.error(`kb-deny-lint: unexpected error: ${err.message}`);
|
|
90
|
+
process.exit(2);
|
|
91
|
+
});
|
package/bin/mcp-server.js
CHANGED
|
@@ -20,14 +20,26 @@ import {
|
|
|
20
20
|
import { WorkspaceConfig } from '../lib/workspace-config.js';
|
|
21
21
|
import { DeSciXApiClient } from '../lib/api-client.js';
|
|
22
22
|
import { WalletFileManager } from '../lib/wallet-file.js';
|
|
23
|
+
import { CLI_VERSION } from '../lib/cli-version.js';
|
|
23
24
|
import * as fs from 'fs/promises';
|
|
24
25
|
import * as path from 'path';
|
|
26
|
+
// WS-MCP-SSOT-TIER2 (audit §5.B-1): the curated HTTP-valid core tools are the SINGLE SOURCE
|
|
27
|
+
// in @descix/platform-api/mcp-tools — shared with the Cloud HTTP MCP path (apiFront.js).
|
|
28
|
+
// This stdio server imports them and CONCATENATES its CLI-LOCAL diagnostics (descix_doctor,
|
|
29
|
+
// platform_health) which are NOT /apifront commands and are intentionally stdio-only.
|
|
30
|
+
// The previously hand-duplicated curated literal in this file is GONE.
|
|
31
|
+
import { toMcpToolList, NATIVE_MCP_TOOLS as SHARED_NATIVE_TOOLS, MCP_HANDSHAKE_INSTRUCTIONS, validateToolParams, toolAcceptsParam, VALIDATION_PHASE } from '@descix/platform-api/mcp-tools';
|
|
25
32
|
|
|
26
33
|
// ---------------------------------------------------------------------------
|
|
27
|
-
//
|
|
34
|
+
// Tool definitions for the stdio MCP transport.
|
|
35
|
+
//
|
|
36
|
+
// CLI-LOCAL diagnostics (descix_doctor, platform_health) are defined here because they are
|
|
37
|
+
// LOCAL CLI operations, not /apifront commands — they must never be advertised over the HTTP
|
|
38
|
+
// path. Everything else comes from the shared SSOT (@descix/platform-api/mcp-tools), which is
|
|
39
|
+
// the same source the Cloud HTTP MCP path advertises. ONE curated list, two transports.
|
|
28
40
|
// ---------------------------------------------------------------------------
|
|
29
41
|
|
|
30
|
-
const
|
|
42
|
+
const CLI_LOCAL_TOOLS = [
|
|
31
43
|
{
|
|
32
44
|
name: 'descix_doctor',
|
|
33
45
|
description:
|
|
@@ -45,92 +57,32 @@ const TOOLS = [
|
|
|
45
57
|
},
|
|
46
58
|
},
|
|
47
59
|
{
|
|
48
|
-
name: '
|
|
49
|
-
description: 'Ask an AI-powered question to an app\'s knowledge base using RAG',
|
|
50
|
-
inputSchema: {
|
|
51
|
-
type: 'object',
|
|
52
|
-
properties: {
|
|
53
|
-
app_id: { type: 'string', description: 'App ID to query' },
|
|
54
|
-
knowledgebase_name: { type: 'string', description: 'KB name (default: General)' },
|
|
55
|
-
user_input: { type: 'string', description: 'The question to ask' },
|
|
56
|
-
},
|
|
57
|
-
required: ['app_id', 'user_input'],
|
|
58
|
-
},
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
name: 'query_knowledge_base',
|
|
62
|
-
description: 'Search a knowledge base using vector similarity (returns raw chunks)',
|
|
63
|
-
inputSchema: {
|
|
64
|
-
type: 'object',
|
|
65
|
-
properties: {
|
|
66
|
-
app_id: { type: 'string', description: 'App ID' },
|
|
67
|
-
kb_id: { type: 'string', description: 'KB name (default: General)' },
|
|
68
|
-
query: { type: 'string', description: 'Search query' },
|
|
69
|
-
limit: { type: 'number', description: 'Max results (default: 5)' },
|
|
70
|
-
},
|
|
71
|
-
required: ['app_id', 'query'],
|
|
72
|
-
},
|
|
73
|
-
},
|
|
74
|
-
{
|
|
75
|
-
name: 'find_communities',
|
|
76
|
-
description: 'List available communities on the platform',
|
|
77
|
-
inputSchema: {
|
|
78
|
-
type: 'object',
|
|
79
|
-
properties: {
|
|
80
|
-
filter: { type: 'string', description: 'Optional filter string' },
|
|
81
|
-
},
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
name: 'list_apps_for_community',
|
|
86
|
-
description: 'List apps in a community',
|
|
87
|
-
inputSchema: {
|
|
88
|
-
type: 'object',
|
|
89
|
-
properties: {
|
|
90
|
-
community_id: { type: 'string', description: 'Community ID' },
|
|
91
|
-
},
|
|
92
|
-
required: ['community_id'],
|
|
93
|
-
},
|
|
94
|
-
},
|
|
95
|
-
{
|
|
96
|
-
name: 'tell_me_how',
|
|
60
|
+
name: 'platform_health',
|
|
97
61
|
description:
|
|
98
|
-
'
|
|
99
|
-
'
|
|
100
|
-
'
|
|
62
|
+
'Check platform health. Returns structured pass/fail JSON for core platform (Cloud) and ' +
|
|
63
|
+
'an optional named microservice. The microservice is probed through the existing MCP proxy ' +
|
|
64
|
+
'infrastructure — no direct HTTP or curl. If this command responds, Cloud is healthy.',
|
|
101
65
|
inputSchema: {
|
|
102
66
|
type: 'object',
|
|
103
67
|
properties: {
|
|
104
|
-
|
|
105
|
-
scope: {
|
|
68
|
+
microservice: {
|
|
106
69
|
type: 'string',
|
|
107
|
-
|
|
108
|
-
description: 'Search scope. "entitlements" = your purchased tools, "discovery" = all platform capabilities. Default: entitlements.',
|
|
70
|
+
description: 'Name of a registered microservice to check (e.g., "beast", "powch"). Omit to check core platform only.',
|
|
109
71
|
},
|
|
110
72
|
},
|
|
111
|
-
required: ['question'],
|
|
112
|
-
},
|
|
113
|
-
},
|
|
114
|
-
{
|
|
115
|
-
name: 'resolve_invite',
|
|
116
|
-
description:
|
|
117
|
-
'Resolve a DeSciX invite token into app configuration. Call this when .descix/app.json ' +
|
|
118
|
-
'contains an invite_token field. Returns app context, community info, and the agent_hint ' +
|
|
119
|
-
'authored by the app creator. The agent_hint tells you about the user\'s skill level and ' +
|
|
120
|
-
'goals — use it to adapt your approach.',
|
|
121
|
-
inputSchema: {
|
|
122
|
-
type: 'object',
|
|
123
|
-
properties: {
|
|
124
|
-
invite_token: {
|
|
125
|
-
type: 'string',
|
|
126
|
-
description: 'The invite token from .descix/app.json',
|
|
127
|
-
},
|
|
128
|
-
},
|
|
129
|
-
required: ['invite_token'],
|
|
130
73
|
},
|
|
131
74
|
},
|
|
132
75
|
];
|
|
133
76
|
|
|
77
|
+
// Final stdio tool list: CLI-local diagnostics + the shared curated core tools (protocol shape).
|
|
78
|
+
const TOOLS = [...CLI_LOCAL_TOOLS, ...toMcpToolList(SHARED_NATIVE_TOOLS)];
|
|
79
|
+
|
|
80
|
+
// Live tool surface as last returned by tools/list (permission-filtered by the backend).
|
|
81
|
+
// Seeded with the static curated SSOT so the very first tools/call is schema-aware even if
|
|
82
|
+
// no tools/list preceded it. Consumed by tools/call for schema-aware context injection and
|
|
83
|
+
// local param pre-validation (ws-mcp-surface-basics).
|
|
84
|
+
let knownTools = TOOLS;
|
|
85
|
+
|
|
134
86
|
// ---------------------------------------------------------------------------
|
|
135
87
|
// Server bootstrap
|
|
136
88
|
// ---------------------------------------------------------------------------
|
|
@@ -150,10 +102,26 @@ try {
|
|
|
150
102
|
console.error('[MCP] No workspace config found — using defaults');
|
|
151
103
|
}
|
|
152
104
|
|
|
153
|
-
// Create HTTP-only API client
|
|
105
|
+
// Create HTTP-only API client.
|
|
106
|
+
//
|
|
107
|
+
// serviceMode: true — THIS SERVER IS HEADLESS AND MUST DECLARE IT. Without it, a session
|
|
108
|
+
// whose token has expired falls through api-client.js's refresh handling into the INTERACTIVE
|
|
109
|
+
// device-login branch (:482-486): it logs to stdout and then `await`s `loginDevice`, opening a
|
|
110
|
+
// browser and blocking — inside a JSON-RPC request, on a transport with no human attached. The
|
|
111
|
+
// correct headless behaviour already existed one branch earlier at :474, which throws an error
|
|
112
|
+
// naming the fix; this server simply never opted into it. Measured 2026-08-30: the branch is
|
|
113
|
+
// reachable from here, and `serviceMode` was false because this construction site never set it.
|
|
114
|
+
//
|
|
115
|
+
// NO baseUrl HERE — and its absence is the fix, not an omission. This site used to pass
|
|
116
|
+
// `baseUrl: workspaceConfig?.apiUrl || null`, reading the LEGACY top-level workspace key. That
|
|
117
|
+
// is a second derivation of the origin, and worse than a preference: api-client.js:60 resolves
|
|
118
|
+
// through the origin owner ONLY `if (!this.baseUrl)`, so supplying one here PREVENTED the owner
|
|
119
|
+
// from ever running — no precedence, no DESCIX_API_URL, and no printed env line. Passing
|
|
120
|
+
// nothing lets the one owner resolve and announce it. (Contract rev 9: ruled a defect against
|
|
121
|
+
// I1 and folded into A1.)
|
|
154
122
|
const apiClient = new DeSciXApiClient({
|
|
155
123
|
projectRoot: workspaceRoot,
|
|
156
|
-
|
|
124
|
+
serviceMode: true,
|
|
157
125
|
});
|
|
158
126
|
|
|
159
127
|
// Load wallet credentials
|
|
@@ -250,6 +218,14 @@ try {
|
|
|
250
218
|
'1. **Objective:** "Do you want to explore existing apps, or build something new?"',
|
|
251
219
|
'2. **Community/App:** Use find_communities + list_apps_for_community, then ask: "Which community and app should this project target?"',
|
|
252
220
|
'3. **Environment:** "Should this run against local dev backend (localhost:4000) or hosted API (descix.net)?"',
|
|
221
|
+
'4. **Cloud Brain:** "Which knowledge bases should I always consult when helping you?"',
|
|
222
|
+
' Use the available_kbs field from this doctor report to show apps the user has access to.',
|
|
223
|
+
' Present each with its app_id and community.',
|
|
224
|
+
' User selects which ones to align to.',
|
|
225
|
+
' Write their selections to .descix/alignment.json:',
|
|
226
|
+
' { "version": "1.0", "apps": [{ "app_id": "...", "kb": "General", "role": "user\'s description" }],',
|
|
227
|
+
' "defaults": { "level": 3, "thinking": -1, "tokens": true } }',
|
|
228
|
+
' If user wants an app they don\'t have: guide them to join the community via find_communities.',
|
|
253
229
|
'',
|
|
254
230
|
'## Canonical setup commands (run in terminal)',
|
|
255
231
|
'```',
|
|
@@ -288,6 +264,43 @@ try {
|
|
|
288
264
|
report.warnings.push('No agent instruction files found. Run "descix quickstart" to generate them.');
|
|
289
265
|
}
|
|
290
266
|
|
|
267
|
+
// Cloud brain alignment — read local alignment config
|
|
268
|
+
const alignmentPath = path.join(checkRoot, '.descix', 'alignment.json');
|
|
269
|
+
try {
|
|
270
|
+
const raw = await fs.readFile(alignmentPath, 'utf-8');
|
|
271
|
+
report.alignment = JSON.parse(raw);
|
|
272
|
+
} catch {
|
|
273
|
+
report.alignment = null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Detect Claude Code environment
|
|
277
|
+
try {
|
|
278
|
+
await fs.access(path.join(checkRoot, '.claude'));
|
|
279
|
+
report.claude_code_detected = true;
|
|
280
|
+
} catch {
|
|
281
|
+
report.claude_code_detected = false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Discover available KBs when authenticated and alignment is needed
|
|
285
|
+
if (walletInfo && (report.setup_needed || !report.alignment)) {
|
|
286
|
+
try {
|
|
287
|
+
const purchasesResult = await apiClient.invoke(
|
|
288
|
+
'fetch_my_purchases',
|
|
289
|
+
{ product_type: 'APP' },
|
|
290
|
+
{ allowGuest: false }
|
|
291
|
+
);
|
|
292
|
+
const purchasedApps = purchasesResult?.message?.apps || [];
|
|
293
|
+
const kbApps = purchasedApps.map(app => ({
|
|
294
|
+
app_id: app.app_id || app.appId || app.id,
|
|
295
|
+
community_id: app.community_id || app.communityId,
|
|
296
|
+
name: app.name || app.app_id || app.appId || app.id,
|
|
297
|
+
})).filter(a => a.app_id);
|
|
298
|
+
report.available_kbs = kbApps;
|
|
299
|
+
} catch {
|
|
300
|
+
report.available_kbs = null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
291
304
|
// Remote verification (if requested, authenticated, and workspace configured)
|
|
292
305
|
if (verifyRemote && walletInfo && freshContext?.appId) {
|
|
293
306
|
try {
|
|
@@ -309,6 +322,15 @@ try {
|
|
|
309
322
|
}
|
|
310
323
|
}
|
|
311
324
|
|
|
325
|
+
// Suggest cloud brain alignment when workspace is configured but alignment is missing
|
|
326
|
+
if (!report.alignment && report.workspace?.configured) {
|
|
327
|
+
report.suggestions = report.suggestions || [];
|
|
328
|
+
report.suggestions.push(
|
|
329
|
+
'Cloud brain alignment not configured. Ask the user which knowledge bases to always consult. ' +
|
|
330
|
+
'Write selections to .descix/alignment.json.'
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
312
334
|
if (report.warnings.length === 0) {
|
|
313
335
|
report.status = 'healthy';
|
|
314
336
|
} else {
|
|
@@ -320,14 +342,40 @@ try {
|
|
|
320
342
|
|
|
321
343
|
// Create MCP server
|
|
322
344
|
const server = new Server(
|
|
323
|
-
{ name: 'DeSciX MCP Server', version:
|
|
324
|
-
|
|
345
|
+
{ name: 'DeSciX MCP Server', version: CLI_VERSION },
|
|
346
|
+
// WS-MVP-FIRSTCONTACT F1: the external-consumer operating manual rides the MCP
|
|
347
|
+
// `initialize` result on BOTH transports. SSOT: @descix/platform-api/mcp-tools
|
|
348
|
+
// handshake.js — do not hand-author a second copy here.
|
|
349
|
+
{ capabilities: { tools: {} }, instructions: MCP_HANDSHAKE_INSTRUCTIONS },
|
|
325
350
|
);
|
|
326
351
|
|
|
327
|
-
// tools/list —
|
|
352
|
+
// tools/list — PERMISSION-FILTERED surface (WS-MCP-SURFACE-SPLIT §10.2).
|
|
353
|
+
//
|
|
354
|
+
// The describe IS the permission gate. Rather than advertise a static curated list, the
|
|
355
|
+
// stdio server asks the BACKEND's /mcp tools/list (authenticated with this operator's
|
|
356
|
+
// credentials) so the advertised surface is filtered by the SAME server-side
|
|
357
|
+
// checkCommandPermission gate as the HTTP transport — ONE filter, no parallel taxonomy.
|
|
358
|
+
// `--admin` is simply the local operator's wallet carrying admin entitlements (cred-gated,
|
|
359
|
+
// §9.1): a platform-admin wallet sees admin tools; a public wallet sees public-only.
|
|
360
|
+
//
|
|
361
|
+
// CLI-LOCAL diagnostics (descix_doctor, platform_health) are ALWAYS concatenated — they are
|
|
362
|
+
// local CLI operations, never /apifront commands, so the backend never advertises them.
|
|
363
|
+
//
|
|
364
|
+
// Fallback: if the backend is unreachable, advertise the static curated SSOT core
|
|
365
|
+
// (CLI_LOCAL_TOOLS + NATIVE_MCP_TOOLS) so the operator is never left with an empty toolbox.
|
|
328
366
|
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
329
|
-
|
|
330
|
-
|
|
367
|
+
try {
|
|
368
|
+
const backendTools = await apiClient.mcpListTools();
|
|
369
|
+
const tools = [...CLI_LOCAL_TOOLS, ...backendTools];
|
|
370
|
+
// Remember the live, permission-filtered surface: tools/call uses it to decide which
|
|
371
|
+
// workspace defaults a tool actually ACCEPTS, and to pre-validate params locally.
|
|
372
|
+
knownTools = tools;
|
|
373
|
+
console.error(`[MCP] tools/list — ${backendTools.length} permission-filtered backend tools + ${CLI_LOCAL_TOOLS.length} CLI-local = ${tools.length}`);
|
|
374
|
+
return { tools };
|
|
375
|
+
} catch (err) {
|
|
376
|
+
console.error(`[MCP] tools/list — backend unreachable (${err.message}); falling back to static curated core (${TOOLS.length} tools)`);
|
|
377
|
+
return { tools: TOOLS };
|
|
378
|
+
}
|
|
331
379
|
});
|
|
332
380
|
|
|
333
381
|
// tools/call — dispatch to backend via HTTP (except doctor which is local)
|
|
@@ -344,16 +392,44 @@ try {
|
|
|
344
392
|
};
|
|
345
393
|
}
|
|
346
394
|
|
|
347
|
-
// Merge default context for convenience (caller can override).
|
|
348
|
-
//
|
|
349
|
-
//
|
|
395
|
+
// Merge default context for convenience (caller can override). Only auto-fill when the
|
|
396
|
+
// workspace is configured — prevents silently targeting the wrong app/community.
|
|
397
|
+
//
|
|
398
|
+
// ws-mcp-surface-basics — SCHEMA-AWARE injection. This block used to inject blindly into
|
|
399
|
+
// EVERY tool, which had two failure modes: it pushed app_id into tools that declare no
|
|
400
|
+
// app_id (find_communities), and it MANUFACTURED `kb_id: 'General'` — a param
|
|
401
|
+
// ask_question_to_app does not accept — so the stdio path itself created the
|
|
402
|
+
// silently-ignored parameter that this workstream exists to kill. A default is now only
|
|
403
|
+
// filled in when the TARGET TOOL DECLARES that parameter. The hardcoded 'General'
|
|
404
|
+
// fallback is DELETED (anti-pattern #7): when the workspace names no KB we send none and
|
|
405
|
+
// let the server apply its own configured default.
|
|
350
406
|
const params = { ...args };
|
|
351
407
|
if (defaultContext) {
|
|
352
|
-
|
|
353
|
-
if (!params.
|
|
354
|
-
if (!params.community_id && defaultContext.communityId) params.community_id = defaultContext.communityId;
|
|
408
|
+
const accepts = (p) => toolAcceptsParam(knownTools, name, p);
|
|
409
|
+
if (!params.app_id && defaultContext.appId && accepts('app_id')) params.app_id = defaultContext.appId;
|
|
410
|
+
if (!params.community_id && defaultContext.communityId && accepts('community_id')) params.community_id = defaultContext.communityId;
|
|
411
|
+
if (!params.kb_id && defaultContext.kbId && accepts('kb_id')) params.kb_id = defaultContext.kbId;
|
|
412
|
+
// The ask-family names the same concept knowledgebase_name; honor the workspace default
|
|
413
|
+
// under the name the target tool actually publishes rather than under kb_id.
|
|
414
|
+
if (!params.knowledgebase_name && defaultContext.kbId && accepts('knowledgebase_name')) {
|
|
415
|
+
params.knowledgebase_name = defaultContext.kbId;
|
|
416
|
+
}
|
|
355
417
|
}
|
|
356
418
|
|
|
419
|
+
// Fail loud LOCALLY on an unknown/missing param, before spending a round trip. The
|
|
420
|
+
// backend re-validates at its own boundary — this is a fast mirror of the same SSOT
|
|
421
|
+
// validator, never a second contract.
|
|
422
|
+
//
|
|
423
|
+
// PRE-INJECTION, measured: `args` is authored by the local MCP client and the only keys this
|
|
424
|
+
// block adds are ones the TARGET TOOL DECLARES (toolAcceptsParam). Nothing platform-injected
|
|
425
|
+
// has been written yet — apiFront's envelope/identity/context writes all happen server-side,
|
|
426
|
+
// after apiClient.invoke leaves this process — so the strict view is the honest one and a
|
|
427
|
+
// client-authored `_descix` is refused here, one round trip early.
|
|
428
|
+
validateToolParams(knownTools, name, params, {
|
|
429
|
+
surface: 'CLI stdio MCP tools/call',
|
|
430
|
+
phase: VALIDATION_PHASE.PRE_INJECTION,
|
|
431
|
+
});
|
|
432
|
+
|
|
357
433
|
const result = await apiClient.invoke(name, params);
|
|
358
434
|
return {
|
|
359
435
|
content: [{
|
|
@@ -363,8 +439,16 @@ try {
|
|
|
363
439
|
};
|
|
364
440
|
} catch (error) {
|
|
365
441
|
console.error(`[MCP] Tool error: ${error.message}`);
|
|
442
|
+
// WS-HEADLESS-MVP-A3: when the backend attached structured error fields (code +
|
|
443
|
+
// data — e.g. CREDITS_REQUIRED with its machine-readable purchasable action), ferry
|
|
444
|
+
// them as JSON after the prose so an MCP client can render the action (balance
|
|
445
|
+
// check via `get_credit_balance`, buy via `descix credits buy` / the
|
|
446
|
+
// create_stripe_checkout_session command) instead of parsing the message string.
|
|
447
|
+
const structured = (error.code || error.data)
|
|
448
|
+
? `\n${JSON.stringify({ code: error.code, data: error.data }, null, 2)}`
|
|
449
|
+
: '';
|
|
366
450
|
return {
|
|
367
|
-
content: [{ type: 'text', text: `Error: ${error.message}` }],
|
|
451
|
+
content: [{ type: 'text', text: `Error: ${error.message}${structured}` }],
|
|
368
452
|
isError: true,
|
|
369
453
|
};
|
|
370
454
|
}
|
package/lib/agent-files.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import * as fs from 'fs/promises';
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import { fileURLToPath } from 'url';
|
|
13
|
+
import { readIdentity, isIdentityNamed } from './workspace-identity.js';
|
|
13
14
|
|
|
14
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
15
16
|
const __dirname = path.dirname(__filename);
|
|
@@ -30,35 +31,46 @@ const AGENT_FILES = [
|
|
|
30
31
|
*/
|
|
31
32
|
async function loadContext(workspaceRoot) {
|
|
32
33
|
const wsPath = path.join(workspaceRoot, '.descix', 'workspace.json');
|
|
34
|
+
|
|
35
|
+
// A workspace.json that cannot be read or parsed is a HARD failure, not a cue to invent an
|
|
36
|
+
// identity. This used to return `{appId: <dirname>, communityId: 'daita', apiUrl: <prod>}` —
|
|
37
|
+
// three fabricated facts written into four files the developer's coding agent then treats as
|
|
38
|
+
// authoritative. 'daita' is the DeSciX root community; handing it to an unrelated developer
|
|
39
|
+
// was the worst of the three.
|
|
40
|
+
let config;
|
|
33
41
|
try {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// Derive API URL
|
|
43
|
-
let apiUrl = config.apiUrl || null;
|
|
44
|
-
if (!apiUrl && platform.microservice?.port) {
|
|
45
|
-
apiUrl = `https://localhost:${platform.microservice.port}`;
|
|
46
|
-
}
|
|
47
|
-
apiUrl = apiUrl || 'https://descix.net';
|
|
42
|
+
config = JSON.parse(await fs.readFile(wsPath, 'utf-8'));
|
|
43
|
+
} catch (err) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Cannot generate agent instruction files: ${wsPath} could not be read (${err.code || err.message}). ` +
|
|
46
|
+
`Run \`descix init -c <community> -a <app>\` first — the app, community and origin these ` +
|
|
47
|
+
`files state must come from your workspace, and there is no default that would be true.`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
// ONE reader, over the keys registerApp() actually writes. See lib/workspace-identity.js.
|
|
52
|
+
const { appId, communityId, apiUrl, originSource } = readIdentity(config);
|
|
51
53
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
appName: path.basename(workspaceRoot),
|
|
60
|
-
};
|
|
54
|
+
if (!isIdentityNamed({ appId, communityId })) {
|
|
55
|
+
const missing = [!appId && 'app id', !communityId && 'community id'].filter(Boolean).join(' and ');
|
|
56
|
+
throw new Error(
|
|
57
|
+
`Cannot generate agent instruction files: ${wsPath} names no ${missing}. ` +
|
|
58
|
+
`Run \`descix init -c <community> -a <app>\` to register it. ` +
|
|
59
|
+
`(Refusing to write files that would state an app or community you did not choose.)`
|
|
60
|
+
);
|
|
61
61
|
}
|
|
62
|
+
|
|
63
|
+
// Under (A', contract rev 2) the origin is NEVER absent: a workspace that named none resolves
|
|
64
|
+
// to the declared default, carrying `source: default`. The "(not configured)" branch that
|
|
65
|
+
// stood here is therefore unreachable and was DELETED rather than fenced — an unreachable
|
|
66
|
+
// branch kept "just in case" is the compat fence CEO-D-2026-07-26 forbids. Which origin this
|
|
67
|
+
// is, and who chose it, is declared by the env line every network-bound command prints.
|
|
68
|
+
const originLine = apiUrl;
|
|
69
|
+
|
|
70
|
+
// Friendly name: capitalize app_id or use as-is
|
|
71
|
+
const appName = appId.charAt(0).toUpperCase() + appId.slice(1);
|
|
72
|
+
|
|
73
|
+
return { appId, communityId, apiUrl: originLine, appName, originSource };
|
|
62
74
|
}
|
|
63
75
|
|
|
64
76
|
/**
|