@descix/cli 1.0.17 → 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.
Files changed (38) hide show
  1. package/agent-assets/guides/README.md +2 -1
  2. package/agent-assets/guides/dynamic-site-pattern.md +6 -10
  3. package/agent-assets/guides/knowledge-only-pattern.md +12 -11
  4. package/agent-assets/guides/microservice-pattern.md +13 -16
  5. package/agent-assets/guides/static-site-pattern.md +6 -7
  6. package/agent-assets/instructions/app-asset-validation.md +61 -59
  7. package/agent-assets/instructions/onboarding-flow.md +57 -74
  8. package/agent-assets/instructions/standard-folder-structure.md +39 -36
  9. package/agent-assets/reference/app-creation.md +27 -33
  10. package/agent-assets/reference/architecture.md +85 -148
  11. package/agent-assets/reference/local-dev.md +5 -83
  12. package/agent-assets/reference/mcp-integration.md +59 -49
  13. package/agent-assets/reference/workspace-config.md +9 -10
  14. package/bin/descix.js +20 -151
  15. package/lib/command-visibility.js +1 -1
  16. package/lib/commands/airdrop.js +6 -1
  17. package/lib/commands/init.js +2 -2
  18. package/lib/commands/kb-sync-surface.js +10 -0
  19. package/lib/commands/kb.js +1 -1
  20. package/lib/commands/microservice-auth-model.js +19 -0
  21. package/lib/commands/retired-verbs.js +106 -0
  22. package/lib/help-guard.js +85 -0
  23. package/lib/service-api-client.js +7 -6
  24. package/lib/verb-invokes.generated.json +0 -5
  25. package/lib/workspace-config.js +1 -1
  26. package/package.json +3 -3
  27. package/templates/agent-claude.md +6 -4
  28. package/templates/agent-cline.md +2 -2
  29. package/templates/agent-copilot.md +2 -2
  30. package/templates/agent-cursor.md +2 -2
  31. package/templates/scaffolds/microservice/SERVICE_README_sdk.md +6 -5
  32. package/templates/scaffolds/microservice/app.js +7 -3
  33. package/templates/scaffolds/microservice/defaults-config.json +2 -1
  34. package/templates/scaffolds/microservice/dev-overrides.example.json +2 -1
  35. package/templates/scaffolds/microservice/manifest.json +2 -1
  36. package/templates/scaffolds/microservice/package.json +2 -2
  37. package/templates/scaffolds/microservice/templates/SERVICE_README_TEMPLATE.md +3 -2
  38. package/lib/commands/retired-kb-sync.js +0 -112
@@ -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
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * A COMMAND THAT DOES NOT EXIST MUST NOT APPEAR TO HAVE HELP.
3
+ *
4
+ * Commander resolves `--help` before it resolves the command, so `descix update --help` printed the
5
+ * TOP-LEVEL help and exited 0 — indistinguishable, to a reader or a script, from a real verb with
6
+ * help. `descix update` alone correctly says `error: unknown command 'update'`.
7
+ *
8
+ * That exit 0 is not only wrong, it TAUGHT something wrong: the daita-sdk docs tell readers that
9
+ * `site upload --help` "silently prints top-level help" and to use `help <sub>` instead. It does no
10
+ * such thing for a real command; the author had been probing a command that did not exist (devx
11
+ * review 2026-09-19, I2 → S5).
12
+ *
13
+ * This guard runs ONLY when a help flag is present, so every other path keeps Commander's own
14
+ * behaviour (it already exits non-zero for an unknown command without `--help`).
15
+ */
16
+
17
+ const HELP_FLAGS = new Set(['-h', '--help']);
18
+
19
+ /** Does this option consume the next argv token as its value? */
20
+ function takesValue(option) {
21
+ return Boolean(option && option.required);
22
+ }
23
+
24
+ function findOption(command, token) {
25
+ const name = token.split('=')[0];
26
+ return command.options.find((o) => o.short === name || o.long === name);
27
+ }
28
+
29
+ function findSubcommand(command, token) {
30
+ return command.commands.find(
31
+ (c) => c.name() === token || (typeof c.aliases === 'function' && c.aliases().includes(token)),
32
+ );
33
+ }
34
+
35
+ /**
36
+ * Walk argv down the command tree and return the first token that should name a subcommand but does
37
+ * not. Returns null when every command token resolves — including when a command takes positional
38
+ * arguments (a command with no subcommands ends the walk, so `descix chat hello` is not "unknown").
39
+ *
40
+ * @param {import('commander').Command} program
41
+ * @param {string[]} argv arguments after the node binary and script (process.argv.slice(2))
42
+ * @returns {{token: string, parent: import('commander').Command}|null}
43
+ */
44
+ export function findUnknownCommand(program, argv) {
45
+ let command = program;
46
+ for (let i = 0; i < argv.length; i++) {
47
+ const token = argv[i];
48
+ if (token === '--') break;
49
+ if (token.startsWith('-')) {
50
+ if (takesValue(findOption(command, token)) && !token.includes('=')) i++;
51
+ continue;
52
+ }
53
+ // A command with no subcommands takes positional arguments from here on.
54
+ if (command.commands.length === 0) break;
55
+ const sub = findSubcommand(command, token);
56
+ if (!sub) return { token, parent: command };
57
+ command = sub;
58
+ }
59
+ return null;
60
+ }
61
+
62
+ /** The command names `parent` actually has, for the "did you mean" line. */
63
+ function nameList(parent) {
64
+ return parent.commands.map((c) => c.name()).sort();
65
+ }
66
+
67
+ /**
68
+ * When argv asks for help on a command that does not exist, refuse by name and exit 1.
69
+ *
70
+ * @param {import('commander').Command} program
71
+ * @param {string[]} argv
72
+ * @param {{ error: (msg: string) => void, exit: (code: number) => void }} io
73
+ */
74
+ export function assertKnownCommandForHelp(program, argv, io = { error: console.error, exit: process.exit }) {
75
+ if (!argv.some((a) => HELP_FLAGS.has(a))) return;
76
+ const unknown = findUnknownCommand(program, argv);
77
+ if (!unknown) return;
78
+
79
+ const path = [];
80
+ for (let c = unknown.parent; c && c.parent; c = c.parent) path.unshift(c.name());
81
+ const where = path.length ? `'descix ${path.join(' ')}'` : 'descix';
82
+ io.error(`error: unknown command '${unknown.token}'`);
83
+ io.error(`${where} has: ${nameList(unknown.parent).join(', ')}`);
84
+ io.exit(1);
85
+ }
@@ -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
- * GITIGNORED dev-overrides.json in dev / a SECRET in prod, NEVER checked-in
17
- * defaults-config.json, because the signature is a credential), and
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. Put it in the ' +
54
- 'GITIGNORED dev-overrides.json (dev) / a secret (prod), never defaults-config.json. No fallback.'
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). Put it in the GITIGNORED dev-overrides.json (dev) / a secret (prod), ' +
61
- 'never defaults-config.json. No fallback.'
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
  ],
@@ -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/retired-kb-sync.js';
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.17",
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.1.8",
34
- "@descix/sdk": "^2.0.0",
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",
@@ -226,8 +226,9 @@ const user = await powch.auth();
226
226
  ### KB Integration
227
227
 
228
228
  If docs already exist (`docs/`, `README.md`, SDK reference files):
229
- - Copy markdown files to `kb/General/` for the sync pipeline
230
- - Keep originals as the source of truth — sync from there
229
+ - Name the docs' folder as a source in a corpus manifest (`.descix/manifests/<KB>.json`), then run
230
+ `descix kb corpus sync`. The docs stay exactly where they are — there is no copy step and no
231
+ required KB folder.
231
232
  - Ask: "I see docs in `docs/`. Want me to use those as your knowledge base?"
232
233
 
233
234
  ### Decision Guide
@@ -236,7 +237,7 @@ If docs already exist (`docs/`, `README.md`, SDK reference files):
236
237
  |-----------------|---------------|-------------|
237
238
  | HTML/JS app with `index.html` | Serve as-is, add DeSciXAppSDK.js | Use any `docs/` or `*.md` files |
238
239
  | React/Vite app | Wrap root in `<AppShell>` | Use `docs/` or README |
239
- | SDK package with docs + demo | Serve the SDK's demo page | Copy docs to `kb/General/` |
240
+ | SDK package with docs + demo | Serve the SDK's demo page | Name docs folder in a corpus manifest |
240
241
  | Docs only (no frontend) | Skip or create minimal site | Docs ARE the KB |
241
242
  | Empty repo | Scaffold from scratch | Create starter KB doc |
242
243
 
@@ -244,7 +245,8 @@ If docs already exist (`docs/`, `README.md`, SDK reference files):
244
245
 
245
246
  Every app has three parts:
246
247
  - `site/` — Frontend (any framework: React, VitePress, vanilla HTML)
247
- - `kb/General/` — Knowledge base source docs (markdown files)
248
+ - A corpus manifest (`.descix/manifests/<KB>.json`) — names the git-tracked docs `descix kb corpus
249
+ sync` publishes; there is no required KB folder
248
250
  - `microservice/` — Backend service (optional)
249
251
 
250
252
  ## App Context
@@ -165,13 +165,13 @@ unverified step — see `.descix/sdk-assets/reference/local-dev.md` §3.3.
165
165
  `DeSciXAppSDK.js` is GENERATED (from `descix-app-sdk/templates/DeSciXAppSDK.template.js`, with a `--check` drift gate) — copy it in, never hand-edit it; a local patch forks the bridge and stops inheriting its fixes.
166
166
  - **React/Vite app:** Wrap root in `<AppShell appId="{{appId}}">` from `@descix/app-sdk/AppShell`.
167
167
  - **Auth only (no DeSciX UI):** Use `PowchClient` from `@descix/app-sdk/powch-client`.
168
- - **Existing docs:** Copy `docs/*.md` to `kb/General/` for sync. Keep originals as source of truth.
168
+ - **Existing docs:** Name the docs' folder as a source in a corpus manifest (`.descix/manifests/<KB>.json`), then `descix kb corpus sync`. Docs stay where they are — no copy step.
169
169
  - **Empty repo:** Scaffold from scratch.
170
170
 
171
171
  ## App Structure
172
172
 
173
173
  - `site/` — Frontend (any framework)
174
- - `kb/General/` — Knowledge base source docs (markdown)
174
+ - A corpus manifest (`.descix/manifests/<KB>.json`) — names the git-tracked docs `descix kb corpus sync` publishes (no required folder)
175
175
  - `microservice/` — Backend service (optional)
176
176
 
177
177
  ## Context
@@ -165,13 +165,13 @@ unverified step — see `.descix/sdk-assets/reference/local-dev.md` §3.3.
165
165
  `DeSciXAppSDK.js` is GENERATED (from `descix-app-sdk/templates/DeSciXAppSDK.template.js`, with a `--check` drift gate) — copy it in, never hand-edit it; a local patch forks the bridge and stops inheriting its fixes.
166
166
  - **React/Vite app:** Wrap root in `<AppShell appId="{{appId}}">` from `@descix/app-sdk/AppShell`.
167
167
  - **Auth only (no DeSciX UI):** Use `PowchClient` from `@descix/app-sdk/powch-client`.
168
- - **Existing docs:** Copy `docs/*.md` to `kb/General/` for sync. Keep originals as source of truth.
168
+ - **Existing docs:** Name the docs' folder as a source in a corpus manifest (`.descix/manifests/<KB>.json`), then `descix kb corpus sync`. Docs stay where they are — no copy step.
169
169
  - **Empty repo:** Scaffold from scratch.
170
170
 
171
171
  ## App Structure
172
172
 
173
173
  - `site/` — Frontend (any framework)
174
- - `kb/General/` — Knowledge base source docs (markdown)
174
+ - A corpus manifest (`.descix/manifests/<KB>.json`) — names the git-tracked docs `descix kb corpus sync` publishes (no required folder)
175
175
  - `microservice/` — Backend service (optional)
176
176
 
177
177
  ## Context
@@ -165,13 +165,13 @@ unverified step — see `.descix/sdk-assets/reference/local-dev.md` §3.3.
165
165
  `DeSciXAppSDK.js` is GENERATED (from `descix-app-sdk/templates/DeSciXAppSDK.template.js`, with a `--check` drift gate) — copy it in, never hand-edit it; a local patch forks the bridge and stops inheriting its fixes.
166
166
  - **React/Vite app:** Wrap root in `<AppShell appId="{{appId}}">` from `@descix/app-sdk/AppShell`.
167
167
  - **Auth only (no DeSciX UI):** Use `PowchClient` from `@descix/app-sdk/powch-client`.
168
- - **Existing docs:** Copy `docs/*.md` to `kb/General/` for sync. Keep originals as source of truth.
168
+ - **Existing docs:** Name the docs' folder as a source in a corpus manifest (`.descix/manifests/<KB>.json`), then `descix kb corpus sync`. Docs stay where they are — no copy step.
169
169
  - **Empty repo:** Scaffold from scratch.
170
170
 
171
171
  ## App Structure
172
172
 
173
173
  - `site/` — Frontend (any framework)
174
- - `kb/General/` — Knowledge base source docs (markdown)
174
+ - A corpus manifest (`.descix/manifests/<KB>.json`) — names the git-tracked docs `descix kb corpus sync` publishes (no required folder)
175
175
  - `microservice/` — Backend service (optional)
176
176
 
177
177
  ## Context
@@ -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 `register-delegate`, NO `SERVICE_KEY`, NO service account, NO OIDC.
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
- > `descix microservice register-delegate` still exists for genuine **service-slot** mesh calls (one service calling another's tools as a delegate). It is NOT used for the app **data plane** — that authenticates as the developer per the model above.
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
- For the app data plane, your service authenticates AS the developer (wallet_address + signature). A 401 means the
338
- developer credential is missing or wrong: check `DEVELOPER_WALLET_ADDRESS` + `DEVELOPER_SIGNATURE` in the gitignored
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 Router
76
- app.use('/api', apiRouter);
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') {
@@ -3,5 +3,6 @@
3
3
  "LOCAL_PORT": "auto",
4
4
  "CORE_API_URL": "https://localhost:4000/apifront",
5
5
  "community_id": "{{COMMUNITY_ID}}",
6
- "app_id": "{{APP_ID}}"
6
+ "app_id": "{{APP_ID}}",
7
+ "MESH_CTX_VERIFY_MODE": "enforce"
7
8
  }
@@ -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
  }
@@ -6,7 +6,8 @@
6
6
  "healthEndpoint": "/health",
7
7
  "debugPort": 3003,
8
8
  "app_id": "{{APP_ID}}",
9
- "community_id": "{{COMMUNITY_ID}}"
9
+ "community_id": "{{COMMUNITY_ID}}",
10
+ "runtime": { "profile": "default", "system_packages": [] }
10
11
  },
11
12
  "commands": {
12
13
  "sample_hello": {
@@ -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.0.0",
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": "^2.0.0",
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
- > `register-delegate`, NO `SERVICE_KEY`, NO service account for the data plane.
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
- }