@descix/cli 1.0.18 → 1.0.19
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/agent-assets/instructions/onboarding-flow.md +0 -1
- package/agent-assets/reference/mcp-integration.md +29 -12
- package/bin/descix.js +15 -151
- package/lib/command-visibility.js +1 -1
- package/lib/commands/airdrop.js +6 -1
- package/lib/commands/init.js +2 -2
- package/lib/commands/kb-sync-surface.js +10 -0
- package/lib/commands/kb.js +1 -1
- package/lib/commands/microservice-auth-model.js +19 -0
- package/lib/commands/retired-verbs.js +106 -0
- package/lib/service-api-client.js +7 -6
- package/lib/verb-invokes.generated.json +0 -5
- package/lib/workspace-config.js +1 -1
- package/package.json +3 -3
- package/templates/scaffolds/microservice/SERVICE_README_sdk.md +6 -5
- package/templates/scaffolds/microservice/app.js +7 -3
- package/templates/scaffolds/microservice/defaults-config.json +2 -1
- package/templates/scaffolds/microservice/dev-overrides.example.json +2 -1
- package/templates/scaffolds/microservice/manifest.json +2 -1
- package/templates/scaffolds/microservice/package.json +2 -2
- package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +3 -2
- package/lib/commands/retired-kb-sync.js +0 -112
|
@@ -10,7 +10,7 @@ This document covers how AI agents interact with the DeSciX platform via MCP (Mo
|
|
|
10
10
|
|
|
11
11
|
## 1. Architecture Overview
|
|
12
12
|
|
|
13
|
-
DeSciX operates as a **Federated MCP Service Mesh**. The DeSciX backend acts as a broker
|
|
13
|
+
DeSciX operates as a **Federated MCP Service Mesh**. The DeSciX backend acts as a broker: it registers tools from all registered microservices alongside its own native commands, dispatches calls to whichever service owns the command, and advertises a small handshake floor of that set at `tools/list` (see §2.2).
|
|
14
14
|
|
|
15
15
|
**Key Innovation:** AI agents discover and invoke capabilities across the entire mesh without knowing where individual services are hosted.
|
|
16
16
|
|
|
@@ -46,14 +46,29 @@ MCP provides the communication protocol between AI agents and the DeSciX platfor
|
|
|
46
46
|
- `ask_question_to_app` - RAG chat with an app's knowledge base (synthesized answer, cited)
|
|
47
47
|
- `query_knowledge_base` - Raw vector search, returns chunks
|
|
48
48
|
|
|
49
|
-
### 2.2
|
|
49
|
+
### 2.2 The Two-Hop: Reaching Commands That Are Not In Your Tool List
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
**Your MCP tool list is a small floor, not the platform's capability surface.** The platform
|
|
52
|
+
registers many more commands than it advertises at the handshake, so that a session pays ~1k
|
|
53
|
+
tokens of catalog instead of ~8.7k. Everything outside the advertised floor stays registered and
|
|
54
|
+
callable — you reach it with two hops:
|
|
52
55
|
|
|
53
|
-
1.
|
|
54
|
-
2.
|
|
55
|
-
|
|
56
|
-
|
|
56
|
+
1. `tell_me_how({ question })` — names the command and its parameters
|
|
57
|
+
2. `execute_remote_command({ command, params })` — invokes it
|
|
58
|
+
|
|
59
|
+
**Rule for an agent: if a capability is not a tool in your list, do not conclude the platform
|
|
60
|
+
lacks it, and do not guess a command name — ask `tell_me_how`, then `execute_remote_command`
|
|
61
|
+
what it names.** This is the standing integration pattern for every caller, on every transport.
|
|
62
|
+
|
|
63
|
+
The advertised floor is a **floor, not a ceiling**: `tools/list` serves it UNION the calling
|
|
64
|
+
identity's granted command names, so a caller may see more, never less. The floor's membership
|
|
65
|
+
and its contract are owned by `describeDiscoveryCoreFence()` /
|
|
66
|
+
`DISCOVERY_CORE_TOOL_NAMES` in `@descix/platform-api`
|
|
67
|
+
(`src/mcp-tools/nativeTools.js`) — read your live `tools/list` rather than a copy of it.
|
|
68
|
+
|
|
69
|
+
The CLI's stdio MCP server additionally concatenates its CLI-local diagnostics (`descix_doctor`,
|
|
70
|
+
`platform_health`) onto whatever the backend advertises; those are local CLI operations, never
|
|
71
|
+
`/apifront` commands, so `execute_remote_command` does not reach them.
|
|
57
72
|
|
|
58
73
|
### 2.3 The `_descix` Context Object
|
|
59
74
|
|
|
@@ -155,21 +170,23 @@ After `descix quickstart` completes:
|
|
|
155
170
|
|
|
156
171
|
### 4.1 Federated MCP Broker
|
|
157
172
|
|
|
158
|
-
The DeSciX backend
|
|
173
|
+
The DeSciX backend registers tools from all registered microservices and makes them callable and
|
|
174
|
+
semantically discoverable — the handshake advertises only the floor (§2.2):
|
|
159
175
|
|
|
160
176
|
```
|
|
161
177
|
Microservice A
|
|
162
178
|
└── manifest.json (tools: [tool1, tool2])
|
|
163
179
|
│
|
|
164
180
|
▼
|
|
165
|
-
Registers with Core
|
|
166
|
-
│
|
|
181
|
+
Registers with Core ──► callable via execute_remote_command
|
|
182
|
+
│ + vectorized for tell_me_how
|
|
167
183
|
▼
|
|
168
184
|
DeSciX Backend (Broker)
|
|
169
|
-
└──
|
|
185
|
+
└── tools/list ──► handshake floor + this caller's grants
|
|
170
186
|
│
|
|
171
187
|
▼
|
|
172
|
-
AI Agent
|
|
188
|
+
AI Agent's tool list: the floor. tool1/tool2 are reached
|
|
189
|
+
with tell_me_how -> execute_remote_command.
|
|
173
190
|
```
|
|
174
191
|
|
|
175
192
|
### 4.2 SERVICE_README Vectorization
|
package/bin/descix.js
CHANGED
|
@@ -11,7 +11,7 @@ import { Command } from 'commander';
|
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import { DeSciXApiClient } from '../lib/api-client.js';
|
|
13
13
|
import { requireAuth } from '../lib/auth-guard.js';
|
|
14
|
-
import { WorkspaceConfig, unmappedAppMessage, resolveWorkspacePath
|
|
14
|
+
import { WorkspaceConfig, unmappedAppMessage, resolveWorkspacePath } from '../lib/workspace-config.js';
|
|
15
15
|
import { CLI_VERSION } from '../lib/cli-version.js';
|
|
16
16
|
import { recordInvocationOrigin } from '../lib/origin.js';
|
|
17
17
|
// Chat session pointer + the ONE rule for when a dead pointer may be self-healed.
|
|
@@ -28,10 +28,12 @@ import * as configCommands from '../lib/commands/config.js';
|
|
|
28
28
|
import * as buyCommands from '../lib/commands/buy.js';
|
|
29
29
|
import * as creditsCommands from '../lib/commands/credits.js';
|
|
30
30
|
import * as airdropCommands from '../lib/commands/airdrop.js';
|
|
31
|
+
import { MICROSERVICE_AUTH_MODEL } from '../lib/commands/microservice-auth-model.js';
|
|
31
32
|
import { runInit } from '../lib/commands/init.js';
|
|
32
33
|
// "May I prompt?" has ONE OWNER. No command in this file derives it.
|
|
33
34
|
import { createPromptSession } from '../lib/interactive.js';
|
|
34
|
-
import {
|
|
35
|
+
import { CANONICAL_KB_SYNC } from '../lib/commands/kb-sync-surface.js';
|
|
36
|
+
import { registerAllRetiredVerbs } from '../lib/commands/retired-verbs.js';
|
|
35
37
|
import { runStatus } from '../lib/commands/status.js';
|
|
36
38
|
import { refreshCommunityIdentity, printIdentityReceipt } from '../lib/commands/communityIdentity.js';
|
|
37
39
|
import { runAppInit } from '../lib/commands/appInit.js';
|
|
@@ -455,9 +457,9 @@ airdropCommand
|
|
|
455
457
|
|
|
456
458
|
// ============ Sync Commands: REMOVED ============
|
|
457
459
|
// `sync assets`, `sync site` and `sync kb` are all gone, so the `sync` GROUP is gone with
|
|
458
|
-
// them: a registered verb with no working children is dead weight. Every retired
|
|
459
|
-
// registered in ONE place below (search
|
|
460
|
-
// in lib/commands/retired-
|
|
460
|
+
// them: a registered verb with no working children is dead weight. Every retired verb is
|
|
461
|
+
// registered in ONE place below (search registerAllRetiredVerbs), driven off the owner table
|
|
462
|
+
// in lib/commands/retired-verbs.js -- this file names no retired verb itself.
|
|
461
463
|
|
|
462
464
|
// ============ Community/App Commands ============
|
|
463
465
|
|
|
@@ -1951,12 +1953,8 @@ kbCommand
|
|
|
1951
1953
|
});
|
|
1952
1954
|
|
|
1953
1955
|
// `kb chunk` and `kb sync` (low-level Git-mode steps) are REMOVED. Their implementations
|
|
1954
|
-
// runKbChunk/runKbSync are deleted from lib/commands/kb.js. Both names
|
|
1955
|
-
//
|
|
1956
|
-
// EVERY retired kb-sync surface is registered here, by ITERATING the owner list -- no verb
|
|
1957
|
-
// name is typed in this file. Adding a surface to RETIRED_KB_SYNC_SURFACES registers it;
|
|
1958
|
-
// nothing can be registered without being in that list.
|
|
1959
|
-
registerAllRetiredKbSync({ program, kb: kbCommand }, chalk.red);
|
|
1956
|
+
// runKbChunk/runKbSync are deleted from lib/commands/kb.js. Both names are rows in
|
|
1957
|
+
// lib/commands/retired-verbs.js, registered with the rest of that table below.
|
|
1960
1958
|
|
|
1961
1959
|
|
|
1962
1960
|
// app records — APP DATA PLANE structured record store (CEO-D-2026-06-02-APP-DATA-PLANE)
|
|
@@ -2929,6 +2927,7 @@ microserviceCommand
|
|
|
2929
2927
|
microserviceCommand
|
|
2930
2928
|
.command('register')
|
|
2931
2929
|
.description('Register microservice with gateway (-r <local SERVICE_README>; git-mode only — no Drive)')
|
|
2930
|
+
.addHelpText('after', `\nAuthentication:\n ${MICROSERVICE_AUTH_MODEL}\n`)
|
|
2932
2931
|
.option('-m, --manifest <path>', 'Path to manifest.json', './manifest.json')
|
|
2933
2932
|
.option('-r, --readme <path>', 'Path to local SERVICE_README file')
|
|
2934
2933
|
.option('-c, --community <id>', 'Community ID (auto-detects from context)')
|
|
@@ -3289,128 +3288,6 @@ microserviceCommand
|
|
|
3289
3288
|
}
|
|
3290
3289
|
});
|
|
3291
3290
|
|
|
3292
|
-
// microservice register-delegate - Register delegate key
|
|
3293
|
-
microserviceCommand
|
|
3294
|
-
.command('register-delegate')
|
|
3295
|
-
.description('Provision the service delegate key (SERVICE_KEY) that authenticates mesh/loopback calls — run this if your service gets HTTP 401 calling /apifront or another service')
|
|
3296
|
-
.option('-c, --community <id>', 'Community ID (auto-detects from context)')
|
|
3297
|
-
.option('-a, --app <id>', 'App ID (auto-detects from context)')
|
|
3298
|
-
.option('-s, --slot <id>', 'Service slot ID (uses first available if not provided)')
|
|
3299
|
-
.action(async (options) => {
|
|
3300
|
-
try {
|
|
3301
|
-
const apiClient = new DeSciXApiClient();
|
|
3302
|
-
await requireAuth(apiClient);
|
|
3303
|
-
|
|
3304
|
-
// Auto-detect context
|
|
3305
|
-
const workspaceConfig = await WorkspaceConfig.load();
|
|
3306
|
-
const ctx = workspaceConfig.resolveContextWithOptions(options);
|
|
3307
|
-
|
|
3308
|
-
let communityId = ctx.communityId;
|
|
3309
|
-
let appId = ctx.appId;
|
|
3310
|
-
|
|
3311
|
-
// Try manifest.json if not detected
|
|
3312
|
-
if (!communityId || !appId) {
|
|
3313
|
-
try {
|
|
3314
|
-
const manifestContent = await fs.readFile('./manifest.json', 'utf-8');
|
|
3315
|
-
const manifest = JSON.parse(manifestContent);
|
|
3316
|
-
if (!communityId) communityId = manifest.service?.community_id;
|
|
3317
|
-
if (!appId) appId = manifest.service?.app_id;
|
|
3318
|
-
} catch {
|
|
3319
|
-
// Not found
|
|
3320
|
-
}
|
|
3321
|
-
}
|
|
3322
|
-
|
|
3323
|
-
if (!communityId || !appId) {
|
|
3324
|
-
console.error(chalk.red('\n❌ Community and App ID required.'));
|
|
3325
|
-
console.log(chalk.gray(' Either provide -c and -a flags, cd into an app directory, or have manifest.json present.\n'));
|
|
3326
|
-
process.exit(1);
|
|
3327
|
-
}
|
|
3328
|
-
|
|
3329
|
-
console.log(chalk.cyan('\n🔑 Registering Service Delegate Key\n'));
|
|
3330
|
-
|
|
3331
|
-
// Fetch entitlements
|
|
3332
|
-
const entitlementsResponse = await apiClient.invoke('fetch_my_purchases', {});
|
|
3333
|
-
const entitlements = entitlementsResponse.message || {};
|
|
3334
|
-
const allSlots = entitlements.service_slots || [];
|
|
3335
|
-
|
|
3336
|
-
// Service slots come ONLY from subscriptions right now. NFT-based slots are FUTURE
|
|
3337
|
-
// functionality (app/NFT association is not wired yet) — never select them here.
|
|
3338
|
-
// CEO-D-2026-06-01-MESH-AUTH-DRIVE-REMOVAL (Fix C).
|
|
3339
|
-
const serviceSlots = allSlots.filter(slot => slot.type === 'subscription');
|
|
3340
|
-
|
|
3341
|
-
if (serviceSlots.length === 0) {
|
|
3342
|
-
console.error(chalk.red('❌ No subscription service slot available.'));
|
|
3343
|
-
console.log(chalk.white(' Service slots are provided by a subscription. A subscription is required to'));
|
|
3344
|
-
console.log(chalk.white(' provision a delegate. (NFT-based slots are future functionality and are'));
|
|
3345
|
-
console.log(chalk.white(' not selectable yet.)'));
|
|
3346
|
-
process.exit(1);
|
|
3347
|
-
}
|
|
3348
|
-
|
|
3349
|
-
// Select slot (subscription slots only)
|
|
3350
|
-
let selectedSlot = serviceSlots[0];
|
|
3351
|
-
if (options.slot) {
|
|
3352
|
-
selectedSlot = serviceSlots.find(s => (s.id || s.nft_id) === options.slot);
|
|
3353
|
-
if (!selectedSlot) {
|
|
3354
|
-
console.error(chalk.red(`❌ Subscription slot ${options.slot} not found in your entitlements.`));
|
|
3355
|
-
console.log(chalk.gray(' Only subscription slots are selectable. Run without -s to use the first available.'));
|
|
3356
|
-
process.exit(1);
|
|
3357
|
-
}
|
|
3358
|
-
}
|
|
3359
|
-
|
|
3360
|
-
console.log(chalk.gray(` Selected slot: ${selectedSlot.name} (${selectedSlot.type})\n`));
|
|
3361
|
-
|
|
3362
|
-
// Generate key pair
|
|
3363
|
-
const crypto = await import('crypto');
|
|
3364
|
-
const { publicKey, privateKey } = crypto.generateKeyPairSync('ec', {
|
|
3365
|
-
namedCurve: 'secp256k1',
|
|
3366
|
-
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
|
3367
|
-
privateKeyEncoding: { type: 'pkcs8', format: 'pem' }
|
|
3368
|
-
});
|
|
3369
|
-
|
|
3370
|
-
console.log(chalk.gray(' Generating key pair...'));
|
|
3371
|
-
|
|
3372
|
-
// Register with Virtual Registry
|
|
3373
|
-
const registerResponse = await apiClient.invoke('register_delegate', {
|
|
3374
|
-
slot_id: selectedSlot.nft_id || selectedSlot.id,
|
|
3375
|
-
slot_type: selectedSlot.type,
|
|
3376
|
-
public_key: publicKey,
|
|
3377
|
-
app_id: appId,
|
|
3378
|
-
community_id: communityId
|
|
3379
|
-
});
|
|
3380
|
-
|
|
3381
|
-
console.log(chalk.green(' ✓ Delegate registered with Core\n'));
|
|
3382
|
-
|
|
3383
|
-
// Save to dev-overrides.json
|
|
3384
|
-
const overridesPath = path.resolve(process.cwd(), 'dev-overrides.json');
|
|
3385
|
-
let overrides = {};
|
|
3386
|
-
|
|
3387
|
-
try {
|
|
3388
|
-
const content = await fs.readFile(overridesPath, 'utf-8');
|
|
3389
|
-
overrides = JSON.parse(content);
|
|
3390
|
-
} catch (e) {
|
|
3391
|
-
// File doesn't exist, start fresh
|
|
3392
|
-
}
|
|
3393
|
-
|
|
3394
|
-
overrides.SERVICE_KEY = {
|
|
3395
|
-
privateKey,
|
|
3396
|
-
publicKey,
|
|
3397
|
-
slotId: selectedSlot.nft_id || selectedSlot.id,
|
|
3398
|
-
slotType: selectedSlot.type,
|
|
3399
|
-
appId: appId,
|
|
3400
|
-
communityId: communityId,
|
|
3401
|
-
createdAt: new Date().toISOString()
|
|
3402
|
-
};
|
|
3403
|
-
|
|
3404
|
-
await fs.writeFile(overridesPath, JSON.stringify(overrides, null, 2));
|
|
3405
|
-
console.log(chalk.green(`✅ Key saved to dev-overrides.json\n`));
|
|
3406
|
-
console.log(chalk.gray(` Path: ${overridesPath}\n`));
|
|
3407
|
-
|
|
3408
|
-
} catch (error) {
|
|
3409
|
-
console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
|
|
3410
|
-
process.exit(1);
|
|
3411
|
-
}
|
|
3412
|
-
});
|
|
3413
|
-
|
|
3414
3291
|
// microservice restart - Kill + relaunch a local microservice (DEV only)
|
|
3415
3292
|
microserviceCommand
|
|
3416
3293
|
.command('restart <name>')
|
|
@@ -3863,24 +3740,6 @@ configCommand
|
|
|
3863
3740
|
}
|
|
3864
3741
|
});
|
|
3865
3742
|
|
|
3866
|
-
// `config set-url` is RETIRED, and it is retired LOUDLY rather than deleted outright: a deleted
|
|
3867
|
-
// subcommand only earns commander's "unknown command", which names no replacement. It used to
|
|
3868
|
-
// assign a top-level `apiUrl` that save() never serialized, then print a success banner over a
|
|
3869
|
-
// value that landed nowhere — the file was re-stamped and the origin never moved. Hidden from
|
|
3870
|
-
// --help; any invocation exits non-zero naming the three surfaces that actually set the origin.
|
|
3871
|
-
configCommand
|
|
3872
|
-
.command('set-url', { hidden: true })
|
|
3873
|
-
.description('retired — see error text')
|
|
3874
|
-
.argument('[url]')
|
|
3875
|
-
.allowUnknownOption()
|
|
3876
|
-
.action(() => {
|
|
3877
|
-
fail(new Error(
|
|
3878
|
-
'"descix config set-url" is retired: it never wrote the origin it reported (a retired top-level\n' +
|
|
3879
|
-
'key that nothing reads). Set the API origin where it is read, env.apiUrl, with one of:\n' +
|
|
3880
|
-
` ${TOP_LEVEL_API_URL_REMEDY}`
|
|
3881
|
-
));
|
|
3882
|
-
});
|
|
3883
|
-
|
|
3884
3743
|
configCommand
|
|
3885
3744
|
.command('init')
|
|
3886
3745
|
.description('Pin this workspace to an environment (requires the global --env dev|demo|prod)')
|
|
@@ -4593,6 +4452,11 @@ program
|
|
|
4593
4452
|
}
|
|
4594
4453
|
});
|
|
4595
4454
|
|
|
4455
|
+
// ============ Retired verbs ============
|
|
4456
|
+
// Every retired verb is a row in lib/commands/retired-verbs.js, registered here in one call once
|
|
4457
|
+
// every parent exists. No retired verb name is typed in this file.
|
|
4458
|
+
registerAllRetiredVerbs({ program, kb: kbCommand, config: configCommand, microservice: microserviceCommand });
|
|
4459
|
+
|
|
4596
4460
|
// ============ Public-vs-admin help listing (CEO ruling, see lib/command-visibility.js) ============
|
|
4597
4461
|
//
|
|
4598
4462
|
// The class comes from the server (`get_command_surface`, guest-allowed) at the moment help is
|
|
@@ -135,7 +135,7 @@ export function applyVisibility(program, surfaceCommands) {
|
|
|
135
135
|
const hasChildren = Array.isArray(cmd.commands) && cmd.commands.length > 0;
|
|
136
136
|
// ADDITIVE ONLY. Some commands are registered `{ hidden: true }` by the CLI itself for
|
|
137
137
|
// reasons that have nothing to do with server admin-gating — see the retired-surface
|
|
138
|
-
// registrations in lib/commands/retired-
|
|
138
|
+
// registrations in lib/commands/retired-verbs.js, each a deliberate loud-refusal stub kept
|
|
139
139
|
// out of the default listing so it doesn't clutter it, while still working and refusing
|
|
140
140
|
// clearly when typed. This layer may HIDE a visible command; it must never REVEAL a command
|
|
141
141
|
// the CLI already chose to hide. MEASURED: an early version unconditionally overwrote
|
package/lib/commands/airdrop.js
CHANGED
|
@@ -205,12 +205,17 @@ export async function executeQueue(options) {
|
|
|
205
205
|
if (result.token_contract_address) console.log(` token_contract: ${result.token_contract_address}`);
|
|
206
206
|
if (result.abi_source) console.log(` abi_source: ${result.abi_source.doc_id} (v${result.abi_source.version || '?'})`);
|
|
207
207
|
console.log(` transfers_total: ${chalk.yellow(result.transfers_total)}`);
|
|
208
|
+
console.log(` batches_total: ${result.batches_total}`);
|
|
208
209
|
console.log(` debit_total: ${result.debit_total}`);
|
|
209
210
|
console.log(` credit_total: ${result.credit_total}`);
|
|
210
|
-
console.log(` bonus_total: ${result.bonus_total}`);
|
|
211
211
|
const nz = result.net_zero_assertion || {};
|
|
212
212
|
const nzLabel = nz.passes ? chalk.green('PASS') : chalk.red('FAIL');
|
|
213
213
|
console.log(` net_zero_assertion: ${nzLabel} (Σ=${nz.sum})`);
|
|
214
|
+
const pbc = result.pre_batch_checks || {};
|
|
215
|
+
console.log(` pre_batch_checks: ${pbc.passes ? chalk.green('PASS') : chalk.red('FAIL — --apply would refuse')}`);
|
|
216
|
+
for (const r of (pbc.refusals || [])) {
|
|
217
|
+
console.log(chalk.red(` ${r.kind} ${r.wallet} (${r.community_id}) holds ${r.held}, needs ${r.required} base units — rows ${(r.pending_ids || []).join(', ')}`));
|
|
218
|
+
}
|
|
214
219
|
console.log(` unique_source_wallets: ${result.unique_source_wallets}`);
|
|
215
220
|
console.log(` unique_master_wallets: ${result.unique_master_wallets}`);
|
|
216
221
|
console.log(` prospective_batch_id: ${result.prospective_batch_id}`);
|
package/lib/commands/init.js
CHANGED
|
@@ -11,10 +11,10 @@ import { fileURLToPath } from 'url';
|
|
|
11
11
|
import { dirname, join } from 'path';
|
|
12
12
|
// clone.js is imported dynamically inside the invite flow to avoid circular deps
|
|
13
13
|
import { WorkspaceConfig } from '../workspace-config.js';
|
|
14
|
-
// The canonical KB-sync surface is owned by
|
|
14
|
+
// The canonical KB-sync surface is owned by kb-sync-surface.js. Consume the constant: a literal
|
|
15
15
|
// spelled here is a second derivation of the same fact, and the removed verb this replaced
|
|
16
16
|
// reached a developer who had done everything right.
|
|
17
|
-
import { CANONICAL_KB_SYNC } from './
|
|
17
|
+
import { CANONICAL_KB_SYNC } from './kb-sync-surface.js';
|
|
18
18
|
// "May I prompt?" has ONE OWNER. init does not derive it, and holds no TTY check of its own.
|
|
19
19
|
import { createLazyPromptSession } from '../interactive.js';
|
|
20
20
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical KB surfaces, named once. Every refusal, hint and next-step that points a
|
|
3
|
+
* developer at KB sync or KB creation consumes these constants rather than retyping them.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/** The single canonical KB sync surface. */
|
|
7
|
+
export const CANONICAL_KB_SYNC = 'descix kb corpus sync';
|
|
8
|
+
|
|
9
|
+
/** The verb that creates a KB — `kb corpus sync`'s dependency. */
|
|
10
|
+
export const CANONICAL_KB_CREATE = 'descix app init --kb <kb_name>';
|
package/lib/commands/kb.js
CHANGED
|
@@ -32,7 +32,7 @@ import * as driveADC from '../google-storage-adc.js';
|
|
|
32
32
|
// The one canonical KB-sync surface, from its owner. Never spell it as a literal here: these
|
|
33
33
|
// commands' next steps are read by a developer as instructions, so they must name the live verb
|
|
34
34
|
// and nothing else.
|
|
35
|
-
import { CANONICAL_KB_SYNC } from './
|
|
35
|
+
import { CANONICAL_KB_SYNC } from './kb-sync-surface.js';
|
|
36
36
|
import { loadSyncState, syncStatePath } from '../core/syncState.js';
|
|
37
37
|
|
|
38
38
|
// ============ Pull Command ============
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE OWNER for how an app microservice authenticates, as the CLI states it.
|
|
3
|
+
*
|
|
4
|
+
* CEO-D-2026-06-02-APP-DATA-PLANE: an app microservice writes on the INJECTED CALLER auth
|
|
5
|
+
* (`params._descix`) with per-app_id isolation, never a per-service delegate key.
|
|
6
|
+
* `microservice register --help` prints this text, and the retired delegate-key verb's refusal
|
|
7
|
+
* (lib/commands/retired-verbs.js) prints it too.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const MICROSERVICE_AUTH_RULING = 'CEO-D-2026-06-02-APP-DATA-PLANE';
|
|
11
|
+
|
|
12
|
+
/** The auth model, printed after `microservice register --help` and in the refusal. */
|
|
13
|
+
export const MICROSERVICE_AUTH_MODEL =
|
|
14
|
+
`A microservice is issued no key of its own (${MICROSERVICE_AUTH_RULING}).\n` +
|
|
15
|
+
` Inbound: /apifront forwards each call with the caller's identity injected as params._descix,\n` +
|
|
16
|
+
` isolated per app_id. Your handler acts on that caller.\n` +
|
|
17
|
+
` Outbound: calls from your service to /apifront authenticate as the developer through\n` +
|
|
18
|
+
` @descix/cli createServiceApiClient (DEVELOPER_WALLET_ADDRESS + DEVELOPER_SIGNATURE).\n` +
|
|
19
|
+
` HTTP 401 on an outbound call: those two developer-credential values are missing or wrong.`;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ONE OWNER for every retired CLI verb: a table and one refusal.
|
|
3
|
+
*
|
|
4
|
+
* A retired verb's implementation is deleted; its NAME stays registered, hidden, so typing it
|
|
5
|
+
* exits non-zero naming the replacement instead of earning commander's "unknown command". That
|
|
6
|
+
* is not a compat fence: nothing here works under an old name.
|
|
7
|
+
*
|
|
8
|
+
* bin/descix.js calls registerAllRetiredVerbs() once and names no retired verb itself, and no
|
|
9
|
+
* other module registers a hidden command (tests/retired-verbs-one-path.test.js). Retiring a verb
|
|
10
|
+
* is one row in RETIRED_VERBS.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { TOP_LEVEL_API_URL_REMEDY } from '../workspace-config.js';
|
|
15
|
+
import { MICROSERVICE_AUTH_MODEL } from './microservice-auth-model.js';
|
|
16
|
+
import { CANONICAL_KB_SYNC, CANONICAL_KB_CREATE } from './kb-sync-surface.js';
|
|
17
|
+
|
|
18
|
+
const kbSyncRefusal = (invocation) =>
|
|
19
|
+
`❌ \`${invocation}\` has been REMOVED. There is no replacement flag and no fallback.\n\n` +
|
|
20
|
+
`Use the one canonical KB sync surface:\n` +
|
|
21
|
+
` ${CANONICAL_KB_SYNC} -a <app_id> [-k <kb_name>]\n\n` +
|
|
22
|
+
`It syncs from a git-tracked corpus manifest (.descix/manifests/), which the removed ` +
|
|
23
|
+
`path could not do. If the KB does not exist yet, create it first with ` +
|
|
24
|
+
`\`${CANONICAL_KB_CREATE}\`.`;
|
|
25
|
+
const KB_SYNC_DESCRIPTION = `REMOVED — use \`${CANONICAL_KB_SYNC}\``;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Every retired verb. `invocation` is what a user types; `parent` is the id of the commander
|
|
29
|
+
* parent it hangs off (a root passed to registerAllRetiredVerbs, or an earlier row); `refusal`
|
|
30
|
+
* renders the full text printed on refusal.
|
|
31
|
+
*/
|
|
32
|
+
export const RETIRED_VERBS = Object.freeze([
|
|
33
|
+
// The KB sync surfaces superseded by `descix kb corpus sync`.
|
|
34
|
+
{ id: 'sync', parent: 'program', name: 'sync', invocation: 'descix sync',
|
|
35
|
+
description: KB_SYNC_DESCRIPTION, refusal: kbSyncRefusal },
|
|
36
|
+
{ id: 'sync.kb', parent: 'sync', name: 'kb', invocation: 'descix sync kb',
|
|
37
|
+
description: KB_SYNC_DESCRIPTION, refusal: kbSyncRefusal },
|
|
38
|
+
{ id: 'kb.chunk', parent: 'kb', name: 'chunk', invocation: 'descix kb chunk',
|
|
39
|
+
description: KB_SYNC_DESCRIPTION, refusal: kbSyncRefusal },
|
|
40
|
+
{ id: 'kb.sync', parent: 'kb', name: 'sync', invocation: 'descix kb sync',
|
|
41
|
+
description: KB_SYNC_DESCRIPTION, refusal: kbSyncRefusal },
|
|
42
|
+
// It assigned a top-level `apiUrl` that save() never serialized and printed success over it.
|
|
43
|
+
{ id: 'config.set-url', parent: 'config', name: 'set-url', invocation: 'descix config set-url',
|
|
44
|
+
description: 'retired — see error text',
|
|
45
|
+
refusal: () =>
|
|
46
|
+
'"descix config set-url" is retired: it never wrote the origin it reported (a retired top-level\n' +
|
|
47
|
+
'key that nothing reads). Set the API origin where it is read, env.apiUrl, with one of:\n' +
|
|
48
|
+
` ${TOP_LEVEL_API_URL_REMEDY}` },
|
|
49
|
+
// There is no per-app microservice delegate key (CEO-D-2026-06-02-APP-DATA-PLANE).
|
|
50
|
+
{ id: 'microservice.register-delegate', parent: 'microservice', name: 'register-delegate',
|
|
51
|
+
invocation: 'descix microservice register-delegate', description: 'removed — see error text',
|
|
52
|
+
refusal: () =>
|
|
53
|
+
'"descix microservice register-delegate" has been removed: there is no per-app microservice ' +
|
|
54
|
+
'delegate key.\n' + MICROSERVICE_AUTH_MODEL },
|
|
55
|
+
]);
|
|
56
|
+
|
|
57
|
+
/** Implementations deleted with the KB sync surfaces. No exported symbol survives without a caller. */
|
|
58
|
+
export const DELETED_KB_SYNC_SYMBOLS = Object.freeze([
|
|
59
|
+
'runKbChunk',
|
|
60
|
+
'runKbSync',
|
|
61
|
+
'runKbBuild',
|
|
62
|
+
'runKbStatus',
|
|
63
|
+
'runKbCompare',
|
|
64
|
+
'updateKB',
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
/** The exact text a retired verb prints. */
|
|
68
|
+
export function retiredVerbRefusal(verb) {
|
|
69
|
+
return verb.refusal(verb.invocation);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** THE refusal: print the verb's text and exit non-zero. Never warns, never falls back. */
|
|
73
|
+
export function refuseRetiredVerb(verb) {
|
|
74
|
+
console.error(chalk.red(`\n${retiredVerbRefusal(verb)}\n`));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Register every row of RETIRED_VERBS, hidden, under its parent. Each accepts and ignores the old
|
|
80
|
+
* arguments and options so an old script fails with the refusal rather than a commander parse
|
|
81
|
+
* error. helpOption(false) is load-bearing: commander answers `--help` BEFORE the action and exits
|
|
82
|
+
* 0, so without it the most natural way to probe a dead verb is the one way it appears to work.
|
|
83
|
+
*
|
|
84
|
+
* @param {Record<string, import('commander').Command>} roots - seed parents by id, e.g. { program, kb, config, microservice }
|
|
85
|
+
* @returns {Record<string, import('commander').Command>} roots plus each registered verb by id
|
|
86
|
+
*/
|
|
87
|
+
export function registerAllRetiredVerbs(roots) {
|
|
88
|
+
const byId = { ...roots };
|
|
89
|
+
for (const verb of RETIRED_VERBS) {
|
|
90
|
+
const parent = byId[verb.parent];
|
|
91
|
+
if (!parent) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`retired-verbs: no parent '${verb.parent}' available for '${verb.invocation}'. ` +
|
|
94
|
+
'Seed it in the roots map or order the table so the parent is registered first.'
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
byId[verb.id] = parent
|
|
98
|
+
.command(verb.name, { hidden: true })
|
|
99
|
+
.description(verb.description)
|
|
100
|
+
.allowUnknownOption(true)
|
|
101
|
+
.helpOption(false)
|
|
102
|
+
.argument('[args...]', 'ignored')
|
|
103
|
+
.action(() => refuseRetiredVerb(verb));
|
|
104
|
+
}
|
|
105
|
+
return byId;
|
|
106
|
+
}
|
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Difference from the CLI: a Cloud Run service has no `.descix/wallet.json` and no TTY. So:
|
|
15
15
|
* - the developer credential is supplied EXPLICITLY (read from the service's config — the
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* app's defaults-config, the developer's choice, or Secret Manager; a defaults-config
|
|
17
|
+
* credential ships inside the image and must never be committed to a public repo), and
|
|
18
18
|
* - on a session-refresh failure it HARD-FAILS (serviceMode) — it never tries interactive
|
|
19
19
|
* device login.
|
|
20
20
|
*
|
|
@@ -50,15 +50,16 @@ export function createServiceApiClient({ baseUrl, walletAddress, signature, user
|
|
|
50
50
|
}
|
|
51
51
|
if (!walletAddress || typeof walletAddress !== 'string') {
|
|
52
52
|
throw new Error(
|
|
53
|
-
'createServiceApiClient: walletAddress is required — the developer credential.
|
|
54
|
-
'
|
|
53
|
+
'createServiceApiClient: walletAddress is required — the developer credential. ' +
|
|
54
|
+
'It may live in the app defaults-config (your choice, e.g. a private repo) or in Secret Manager. ' +
|
|
55
|
+
'A credential in defaults-config ships inside the image; never commit it to a repo that is or will be public. No fallback.'
|
|
55
56
|
);
|
|
56
57
|
}
|
|
57
58
|
if (!signature || typeof signature !== 'string') {
|
|
58
59
|
throw new Error(
|
|
59
60
|
'createServiceApiClient: signature is required — the developer durable credential (from their ' +
|
|
60
|
-
'.descix/wallet.json).
|
|
61
|
-
'
|
|
61
|
+
'.descix/wallet.json). It may live in the app defaults-config (your choice, e.g. a private repo) or in Secret Manager. ' +
|
|
62
|
+
'A credential in defaults-config ships inside the image; never commit it to a repo that is or will be public. No fallback.'
|
|
62
63
|
);
|
|
63
64
|
}
|
|
64
65
|
|
|
@@ -103,7 +103,6 @@
|
|
|
103
103
|
"config set-gateway-port": [],
|
|
104
104
|
"config set-powch-url": [],
|
|
105
105
|
"config set-site-url": [],
|
|
106
|
-
"config set-url": [],
|
|
107
106
|
"config show": [],
|
|
108
107
|
"credits balance": [
|
|
109
108
|
"get_credit_balance"
|
|
@@ -174,10 +173,6 @@
|
|
|
174
173
|
"microservice register": [
|
|
175
174
|
"register_service"
|
|
176
175
|
],
|
|
177
|
-
"microservice register-delegate": [
|
|
178
|
-
"fetch_my_purchases",
|
|
179
|
-
"register_delegate"
|
|
180
|
-
],
|
|
181
176
|
"microservice reload": [
|
|
182
177
|
"reload_service_manifests"
|
|
183
178
|
],
|
package/lib/workspace-config.js
CHANGED
|
@@ -5,7 +5,7 @@ import { resolveOrigin } from './origin.js';
|
|
|
5
5
|
// The one canonical KB-sync surface, from its owner. The example in requireContext() is a remedy
|
|
6
6
|
// a stuck developer will copy verbatim, so it must resolve to the live verb rather than a literal
|
|
7
7
|
// that can go stale where nobody is watching.
|
|
8
|
-
import { CANONICAL_KB_SYNC } from './commands/
|
|
8
|
+
import { CANONICAL_KB_SYNC } from './commands/kb-sync-surface.js';
|
|
9
9
|
import { npxCommand } from './invocation.js';
|
|
10
10
|
|
|
11
11
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@descix/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.19",
|
|
4
4
|
"description": "DeSciX CLI and MCP Server - Unified command-line interface for DeSciX platform",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
"author": "DeSciX",
|
|
31
31
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@descix/app-sdk": "^0.
|
|
34
|
-
"@descix/sdk": "^
|
|
33
|
+
"@descix/app-sdk": "^0.2.0",
|
|
34
|
+
"@descix/sdk": "^3.0.0",
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.25.1",
|
|
36
36
|
"axios": "^1.7.9",
|
|
37
37
|
"chalk": "^5.6.2",
|
|
@@ -153,7 +153,7 @@ descix kb corpus sync -a myapp -k General
|
|
|
153
153
|
|
|
154
154
|
### Calling /apifront from your microservice (data plane)
|
|
155
155
|
|
|
156
|
-
**The microservice IS the CLI's api-client running in the cloud** (CEO-D-2026-06-02-APP-MICROSERVICE-IS-CLI-CLIENT-WALLET-SIG). To call Core commands (`app_records_*`, `get_app_asset`, `get_asset_upload_token`, RAG, ...) the scaffold's `mcpClient` authenticates to `/apifront` **exactly like the CLI**: it holds the **developer's own durable credential** (`wallet_address` + `signature`) and calls `reconnect_by_wallet` to mint a session — then makes calls **as the developer**. There is NO
|
|
156
|
+
**The microservice IS the CLI's api-client running in the cloud** (CEO-D-2026-06-02-APP-MICROSERVICE-IS-CLI-CLIENT-WALLET-SIG). To call Core commands (`app_records_*`, `get_app_asset`, `get_asset_upload_token`, RAG, ...) the scaffold's `mcpClient` authenticates to `/apifront` **exactly like the CLI**: it holds the **developer's own durable credential** (`wallet_address` + `signature`) and calls `reconnect_by_wallet` to mint a session — then makes calls **as the developer**. There is NO per-service key, NO service account, NO OIDC.
|
|
157
157
|
|
|
158
158
|
**Provide the developer credential (gitignored / secret — never checked in):**
|
|
159
159
|
1. Copy `dev-overrides.example.json` → `dev-overrides.json` (this file is `.gitignore`d).
|
|
@@ -170,7 +170,7 @@ descix microservice register
|
|
|
170
170
|
# then: cp dev-overrides.example.json dev-overrides.json and fill in the developer credential
|
|
171
171
|
```
|
|
172
172
|
|
|
173
|
-
|
|
173
|
+
**Inbound calls carry the caller, not a service identity** (CEO-D-2026-06-02-APP-DATA-PLANE). `/apifront` forwards each call to your service with the caller's identity injected as `params._descix`, isolated per `app_id`; your handler acts on that caller. `descix microservice register --help` prints this model.
|
|
174
174
|
|
|
175
175
|
---
|
|
176
176
|
|
|
@@ -334,9 +334,10 @@ A single-app (non-monorepo) workspace uses the same v2.1 shape with only `env.pl
|
|
|
334
334
|
3. Check `.vscode/mcp.json` exists with a `descix` server (this file is skipped, by design, if the DeSciX VS Code extension is installed — it registers MCP natively)
|
|
335
335
|
|
|
336
336
|
### "401 Unauthorized on a loopback / mesh call"
|
|
337
|
-
|
|
338
|
-
developer credential is missing or wrong: check `DEVELOPER_WALLET_ADDRESS` + `DEVELOPER_SIGNATURE` in the
|
|
339
|
-
`dev-overrides.json` (dev) / the secret (prod). Then restart the service.
|
|
337
|
+
Outbound, your service authenticates AS the developer (wallet_address + signature) through `createServiceApiClient`. A 401
|
|
338
|
+
means the developer credential is missing or wrong: check `DEVELOPER_WALLET_ADDRESS` + `DEVELOPER_SIGNATURE` in the
|
|
339
|
+
gitignored `dev-overrides.json` (dev) / the secret (prod). Then restart the service. Inbound, the caller's identity
|
|
340
|
+
arrives as `params._descix`; no key is provisioned for your service.
|
|
340
341
|
Discover it via `descix tell-me-how "my service gets 401 on a loopback call"`.
|
|
341
342
|
|
|
342
343
|
### "Session expired"
|
|
@@ -8,7 +8,7 @@ import express from 'express';
|
|
|
8
8
|
import http from 'http';
|
|
9
9
|
import cors from 'cors';
|
|
10
10
|
import { initializeServiceConfig, utils } from './services/utils.js';
|
|
11
|
-
import { killExistingProcess } from '@descix/cloud-core';
|
|
11
|
+
import { killExistingProcess, mountMeshApi } from '@descix/cloud-core';
|
|
12
12
|
import apiRouter from './services/apiFront.js';
|
|
13
13
|
|
|
14
14
|
const app = express();
|
|
@@ -72,8 +72,12 @@ initializeServiceConfig().then(() => {
|
|
|
72
72
|
res.status(200).send('OK');
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
-
// Mount API
|
|
76
|
-
|
|
75
|
+
// Mount the API router on /api — the surface the platform proxies mesh commands to. The
|
|
76
|
+
// mount is @descix/cloud-core's: it puts the signed-context verifier in front of the router,
|
|
77
|
+
// in the mode config resolved at boot. A deployed service boots only with
|
|
78
|
+
// MESH_CTX_VERIFY_MODE=enforce (defaults-config.json), so an unsigned request is refused
|
|
79
|
+
// before it reaches a handler. Do not mount /api yourself.
|
|
80
|
+
mountMeshApi(app, apiRouter);
|
|
77
81
|
|
|
78
82
|
// Start Server
|
|
79
83
|
if (!port || port === 'auto') {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Copy this file to dev-overrides.json (GITIGNORED) and fill in the developer credential. The microservice authenticates to /apifront AS the developer — exactly like the CLI — using these. In prod these come from a SECRET, never from a checked-in file. defaults-config.json must NOT contain the signature (it is a credential).",
|
|
3
3
|
"DEVELOPER_WALLET_ADDRESS": "0xYOUR_WALLET_ADDRESS",
|
|
4
|
-
"DEVELOPER_SIGNATURE": "PASTE_YOUR_DURABLE_SIGNATURE_FROM_.descix/wallet.json"
|
|
4
|
+
"DEVELOPER_SIGNATURE": "PASTE_YOUR_DURABLE_SIGNATURE_FROM_.descix/wallet.json",
|
|
5
|
+
"_MESH_CTX_VERIFY_MODE_comment": "defaults-config.json sets MESH_CTX_VERIFY_MODE to enforce, the only value a deployed service boots with. On your own machine you may relax it here: \"warn\" (verify and log, never refuse) or \"\" (no verification; every request is stamped meshContextVerified=false). Never set MESH_CTX_PUBLIC_KEY or MESH_CTX_KEY_ID in any config file: the platform key is resolved by @descix/cloud-core and a copy refuses boot."
|
|
5
6
|
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"gen-jwt-secret": "node -e 'console.log(require(\"crypto\").randomBytes(32).toString(\"hex\"))'"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@descix/cloud-core": "^1.
|
|
14
|
+
"@descix/cloud-core": "^1.2.0",
|
|
15
15
|
"@descix/platform-api": "^2.0.0",
|
|
16
16
|
"@google-cloud/secret-manager": "^5.0.1",
|
|
17
17
|
"@google-cloud/storage": "^7.7.0",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"cors": "^2.8.5",
|
|
20
20
|
"dotenv": "^16.3.1",
|
|
21
21
|
"express": "^4.18.2",
|
|
22
|
-
"@descix/sdk": "^
|
|
22
|
+
"@descix/sdk": "^3.0.0",
|
|
23
23
|
"@descix/cli": "^1.0.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
@@ -25,8 +25,9 @@ descix microservice register # register the service manifest
|
|
|
25
25
|
> + `DEVELOPER_SIGNATURE` (the developer's own wallet credential, from
|
|
26
26
|
> `.descix/wallet.json`) and calls `reconnect_by_wallet` to mint a session —
|
|
27
27
|
> exactly like the CLI. Put the credential in the GITIGNORED `dev-overrides.json`
|
|
28
|
-
> (dev) / a secret (prod), NEVER `defaults-config.json`. There is NO
|
|
29
|
-
>
|
|
28
|
+
> (dev) / a secret (prod), NEVER `defaults-config.json`. There is NO per-service
|
|
29
|
+
> key and NO service account for the data plane. Inbound calls carry the caller's
|
|
30
|
+
> identity as `params._descix`, isolated per app_id (CEO-D-2026-06-02-APP-DATA-PLANE).
|
|
30
31
|
> **HTTP 401?** The developer credential is missing or wrong — check those two keys.
|
|
31
32
|
|
|
32
33
|
## Prerequisites
|
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ONE OWNER for the retirement of every superseded KB-sync surface.
|
|
3
|
-
*
|
|
4
|
-
* CEO 2026-08-29: "there should never be a deprecated path and then everything else is a
|
|
5
|
-
* waterfall of problems." The CLI exposes exactly ONE kb sync surface — `descix kb corpus
|
|
6
|
-
* sync` (git-synced manifest). Every other sync path is DELETED: its implementation is gone
|
|
7
|
-
* and its old name exits NON-ZERO naming the replacement. That is not a compat fence — a
|
|
8
|
-
* compat fence is a path that still WORKS under an old name. These do not work at all.
|
|
9
|
-
*
|
|
10
|
-
* Why a module and not a literal at each site: two derivations of one fact is the general
|
|
11
|
-
* form of mirror drift. The retired set is enumerated ONCE here. bin/descix.js registers by
|
|
12
|
-
* ITERATING this list via registerAllRetiredKbSync() -- it does not name any retired verb
|
|
13
|
-
* itself -- and tests/kb-sync-single-surface.test.js asserts against the same list. So the
|
|
14
|
-
* drift is closed in BOTH directions: a surface added to the list is registered, and a
|
|
15
|
-
* surface cannot be registered without being in the list.
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/** The single canonical KB sync surface. Named in every refusal. */
|
|
19
|
-
export const CANONICAL_KB_SYNC = 'descix kb corpus sync';
|
|
20
|
-
|
|
21
|
-
/** The verb that creates a KB — `kb corpus sync`'s dependency (I4). */
|
|
22
|
-
export const CANONICAL_KB_CREATE = 'descix app init --kb <kb_name>';
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Every KB-sync surface retired by ws-devplane-cli-kb-surface-single-canonical-sync.
|
|
26
|
-
* `invocation` is what a user types; `parent` is the commander parent it hung off.
|
|
27
|
-
*/
|
|
28
|
-
export const RETIRED_KB_SYNC_SURFACES = [
|
|
29
|
-
{ id: 'sync', parent: 'program', name: 'sync', invocation: 'descix sync' },
|
|
30
|
-
{ id: 'sync.kb', parent: 'sync', name: 'kb', invocation: 'descix sync kb' },
|
|
31
|
-
{ id: 'kb.chunk', parent: 'kb', name: 'chunk', invocation: 'descix kb chunk' },
|
|
32
|
-
{ id: 'kb.sync', parent: 'kb', name: 'sync', invocation: 'descix kb sync' },
|
|
33
|
-
];
|
|
34
|
-
|
|
35
|
-
/** Implementations deleted with those surfaces. No exported symbol survives without a caller (I3). */
|
|
36
|
-
export const DELETED_KB_SYNC_SYMBOLS = [
|
|
37
|
-
'runKbChunk',
|
|
38
|
-
'runKbSync',
|
|
39
|
-
'runKbBuild',
|
|
40
|
-
'runKbStatus',
|
|
41
|
-
'runKbCompare',
|
|
42
|
-
'updateKB',
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* The single refusal message. Fails loud and names the replacement — never warns, never falls back.
|
|
47
|
-
* @param {string} invocation - the retired invocation the user typed, e.g. 'descix kb chunk'
|
|
48
|
-
* @returns {string}
|
|
49
|
-
*/
|
|
50
|
-
export function retiredKbSyncMessage(invocation) {
|
|
51
|
-
return (
|
|
52
|
-
`\`${invocation}\` has been REMOVED. There is no replacement flag and no fallback.\n\n` +
|
|
53
|
-
`Use the one canonical KB sync surface:\n` +
|
|
54
|
-
` ${CANONICAL_KB_SYNC} -a <app_id> [-k <kb_name>]\n\n` +
|
|
55
|
-
`It syncs from a git-tracked corpus manifest (.descix/manifests/), which the removed ` +
|
|
56
|
-
`path could not do. If the KB does not exist yet, create it first with ` +
|
|
57
|
-
`\`${CANONICAL_KB_CREATE}\`.`
|
|
58
|
-
);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Register a retired invocation so it exits NON-ZERO naming the canonical surface.
|
|
63
|
-
* Accepts and ignores any args/options the old surface took, so an old script fails with
|
|
64
|
-
* this message rather than a confusing commander parse error.
|
|
65
|
-
*
|
|
66
|
-
* @param {import('commander').Command} parent - commander parent to attach to
|
|
67
|
-
* @param {string} name - the retired command name
|
|
68
|
-
* @param {string} invocation - full invocation used in the message
|
|
69
|
-
* @param {(s:string)=>string} red - chalk.red or equivalent
|
|
70
|
-
*/
|
|
71
|
-
export function registerRetiredKbSync(parent, name, invocation, red) {
|
|
72
|
-
return parent
|
|
73
|
-
.command(name, { hidden: true })
|
|
74
|
-
.description(`REMOVED — use \`${CANONICAL_KB_SYNC}\``)
|
|
75
|
-
.allowUnknownOption(true)
|
|
76
|
-
// helpOption(false) is load-bearing: commander intercepts `--help` BEFORE the action and
|
|
77
|
-
// exits 0. A retired surface must exit NON-ZERO however it is invoked — `descix kb chunk`
|
|
78
|
-
// and `descix kb chunk --help` must both fail and name the replacement. Without this, the
|
|
79
|
-
// most natural way a user probes a dead verb is the one way it appears to still work.
|
|
80
|
-
.helpOption(false)
|
|
81
|
-
.argument('[args...]', 'ignored')
|
|
82
|
-
.action(() => {
|
|
83
|
-
console.error(red(`\n❌ ${retiredKbSyncMessage(invocation)}\n`));
|
|
84
|
-
process.exit(1);
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Register EVERY retired surface by iterating RETIRED_KB_SYNC_SURFACES.
|
|
90
|
-
*
|
|
91
|
-
* This is what makes the one-owner claim true rather than merely asserted: bin/descix.js
|
|
92
|
-
* calls this once and never names a retired verb itself, so a retired surface cannot be
|
|
93
|
-
* registered without appearing in the list above.
|
|
94
|
-
*
|
|
95
|
-
* @param {Record<string, import('commander').Command>} roots - seed parents, e.g. { program, kb }
|
|
96
|
-
* @param {(s:string)=>string} red - chalk.red or equivalent
|
|
97
|
-
* @returns {Record<string, import('commander').Command>} roots plus each registered surface by id
|
|
98
|
-
*/
|
|
99
|
-
export function registerAllRetiredKbSync(roots, red) {
|
|
100
|
-
const byId = { ...roots };
|
|
101
|
-
for (const s of RETIRED_KB_SYNC_SURFACES) {
|
|
102
|
-
const parent = byId[s.parent];
|
|
103
|
-
if (!parent) {
|
|
104
|
-
throw new Error(
|
|
105
|
-
`retired-kb-sync: no parent '${s.parent}' available for '${s.invocation}'. ` +
|
|
106
|
-
'Seed it in the roots map or order the list so the parent is registered first.'
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
byId[s.id] = registerRetiredKbSync(parent, s.name, s.invocation, red);
|
|
110
|
-
}
|
|
111
|
-
return byId;
|
|
112
|
-
}
|