biz-a-cli 2.3.80-15385 → 2.3.80-15397
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/bin/hub.js +5 -0
- package/bin/hubEvent.js +118 -53
- package/bin/tunnelTarget.js +60 -7
- package/engine/bpm/workflow-runtime.js +3 -3
- package/engine/cc/aiTurns.js +394 -0
- package/engine/cc/appMenus.d.ts +2 -0
- package/engine/cc/appMenus.js +14 -0
- package/engine/cc/ccGate.js +15 -0
- package/engine/cc/configTools.js +627 -0
- package/engine/cc/configValidate.js +85 -0
- package/engine/cc/currentConfig.js +31 -0
- package/engine/cc/derive.js +178 -0
- package/engine/cc/draftStore.js +2 -8
- package/engine/cc/drift.js +256 -0
- package/engine/cc/gapRepo.js +54 -0
- package/engine/cc/layer3Behavior.js +54 -0
- package/engine/cc/layer3Schema.d.ts +39 -0
- package/engine/cc/layer3Schema.js +153 -0
- package/engine/cc/mapCommit.js +77 -0
- package/engine/cc/mapPrint.d.ts +53 -0
- package/engine/cc/mapPrint.js +519 -0
- package/engine/cc/mapRepo.js +80 -0
- package/engine/cc/mapStore.js +190 -0
- package/engine/cc/mapTools.js +334 -0
- package/engine/cc/phaseA.js +164 -0
- package/engine/cc/phaseB.js +228 -0
- package/engine/cc/processMap.d.ts +79 -0
- package/engine/cc/processMap.js +470 -0
- package/engine/cc/prompts/derive.js +49 -0
- package/engine/cc/prompts/phaseA.js +41 -0
- package/engine/cc/prompts/phaseB.js +30 -0
- package/engine/cc/traceRepo.js +102 -0
- package/engine/cc/traceTargets.js +66 -0
- package/engine/cc/translate.js +96 -0
- package/engine/cc/turnCommit.js +157 -0
- package/engine/cc/turnLoop.js +87 -0
- package/engine/cc/turnRepo.js +141 -0
- package/engine/orm/apiRoute.js +2 -1
- package/engine/orm/principalGuards.js +2 -1
- package/engine/orm/registryClient.js +18 -6
- package/engine/orm/userPreferences.js +2 -1
- package/envs/env.dev.js +4 -5
- package/envs/env.js +9 -5
- package/migrations/postgres/1790300000000__cc_trace_baseline.sql +20 -0
- package/package.json +2 -3
- package/readme.md +17 -1
- package/scheduler/configController.js +57 -98
- package/scheduler/timer.js +4 -3
- package/security/aiGatewayClient.js +9 -1
- package/config/databases.json +0 -20
- package/config/ext-oidc.json +0 -7
- package/config/secrets.json +0 -27
package/bin/hub.js
CHANGED
|
@@ -130,6 +130,11 @@ const argv = yargs(process.argv.slice(2))
|
|
|
130
130
|
show its rules behave. */
|
|
131
131
|
default: "observe",
|
|
132
132
|
})
|
|
133
|
+
.options("aiGateway", {
|
|
134
|
+
describe:
|
|
135
|
+
"Doc 1 0b.6b: base URL of the central AI gateway (biz-a-server /ai) that ccai calls. Defaults to this hub's server (serverMode).",
|
|
136
|
+
type: "string",
|
|
137
|
+
})
|
|
133
138
|
.options("services", {
|
|
134
139
|
describe:
|
|
135
140
|
"Comma-separated list of engine jobs this agent handles (e.g., 'BPM,EMAIL')",
|
package/bin/hubEvent.js
CHANGED
|
@@ -28,6 +28,14 @@ import { authorizeRequest, principalForCliScript } from "../security/internalPri
|
|
|
28
28
|
import { whoami } from "../security/whoami.js";
|
|
29
29
|
import { runDraftAction } from "../engine/cc/draftStore.js";
|
|
30
30
|
import { createPgDraftRepo } from "../engine/cc/pgRepo.js";
|
|
31
|
+
import { runMapAction } from "../engine/cc/mapStore.js";
|
|
32
|
+
import { createMapRepo } from "../engine/cc/mapRepo.js";
|
|
33
|
+
import { createGapRepo } from "../engine/cc/gapRepo.js";
|
|
34
|
+
import { createTraceRepo } from "../engine/cc/traceRepo.js";
|
|
35
|
+
import { createConfigReader } from "../engine/cc/currentConfig.js";
|
|
36
|
+
import { runAiAction } from "../engine/cc/aiTurns.js";
|
|
37
|
+
import { createTurnRepo } from "../engine/cc/turnRepo.js";
|
|
38
|
+
import { createAiGatewayClient } from "../security/aiGatewayClient.js";
|
|
31
39
|
import {
|
|
32
40
|
readUserPreferences,
|
|
33
41
|
writeUserPreferences,
|
|
@@ -47,7 +55,7 @@ import {
|
|
|
47
55
|
execPostgresTransaction,
|
|
48
56
|
isPostgresIndex,
|
|
49
57
|
} from "../engine/domain/dialect.js";
|
|
50
|
-
import {
|
|
58
|
+
import { pipeTunnel } from "./tunnelTarget.js";
|
|
51
59
|
/*
|
|
52
60
|
* Doc 4 §8 — ⚠️ `runAsPrincipal` VALIDATES SYNCHRONOUSLY, so a browser-supplied active tenant outside
|
|
53
61
|
* the signed claim throws out of the CALL, not out of the promise it would have returned. On this
|
|
@@ -232,59 +240,34 @@ export const streamEvent = async (socket, argv) =>
|
|
|
232
240
|
},
|
|
233
241
|
});
|
|
234
242
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
socket.once(clientId, () => {
|
|
264
|
-
// hub server shall notify us to end pipeline as soon as possible
|
|
265
|
-
client.end();
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
ss(socket).emit(clientId, s);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
client.setTimeout(IDLE_SOCKET_TIMEOUT_MILLISECONDS);
|
|
272
|
-
client.on("timeout", () => {
|
|
273
|
-
// in case client not notify to end pipeline, then inactivity timeout will end it
|
|
274
|
-
client.end();
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
client.on("error", (err) => {
|
|
278
|
-
// handle connection refusal (create a stream and immediately close it)
|
|
279
|
-
logger.error(
|
|
280
|
-
`API Error : tunnel could not reach ${target.local ? "this CLI's own endpoint" : "the API"} at ` +
|
|
281
|
-
`${target.host}:${target.port} — `,
|
|
282
|
-
err,
|
|
283
|
-
);
|
|
284
|
-
let s = ss.createStream();
|
|
285
|
-
ss(socket).emit(clientId, s);
|
|
286
|
-
s.end();
|
|
243
|
+
/*
|
|
244
|
+
* Where this pipe ends is decided by the request's FIRST BYTES (bin/tunnelTarget.js): a tenant
|
|
245
|
+
* this CLI serves on PostgreSQL, and — since 2026-10-01 — a Firebird tenant's LOGIN, are looped
|
|
246
|
+
* back to the hub's own FINA-compatible endpoint; every other request goes to FINA unchanged.
|
|
247
|
+
* Without the loop-back a Firebird login never passed through the CLI, so it never received
|
|
248
|
+
* the signed principal (Doc 6 on Firebird).
|
|
249
|
+
*/
|
|
250
|
+
const s = ss.createStream();
|
|
251
|
+
pipeTunnel({
|
|
252
|
+
stream: s,
|
|
253
|
+
argv,
|
|
254
|
+
isPostgresIndex,
|
|
255
|
+
connect: (target) =>
|
|
256
|
+
target.secure
|
|
257
|
+
? tls.connect({
|
|
258
|
+
port: target.port,
|
|
259
|
+
host: target.host,
|
|
260
|
+
// Looping back to ourselves, the certificate is our own.
|
|
261
|
+
rejectUnauthorized: !target.local,
|
|
262
|
+
})
|
|
263
|
+
: net.connect(target.port, target.host),
|
|
264
|
+
transform: () => addCLIAddressAsResponseHeader,
|
|
265
|
+
idleMs: IDLE_SOCKET_TIMEOUT_MILLISECONDS,
|
|
266
|
+
// hub server shall notify us to end pipeline as soon as possible
|
|
267
|
+
onClient: (client) => socket.once(clientId, () => client.end()),
|
|
268
|
+
logger,
|
|
287
269
|
});
|
|
270
|
+
ss(socket).emit(clientId, s);
|
|
288
271
|
};
|
|
289
272
|
|
|
290
273
|
// const cliReqCb = async (data, callback) => {
|
|
@@ -480,6 +463,31 @@ const EngineRegistry = {
|
|
|
480
463
|
import("../engine/domain/applicationConfig.js"),
|
|
481
464
|
};
|
|
482
465
|
|
|
466
|
+
/* Doc 1 step 3 — what a Phase B turn needs: the draft store, the gap list, a draft opener that applies the
|
|
467
|
+
draft store's own rules (Layer 3, owned by Control Center) for the VERIFIED caller, one transaction, and
|
|
468
|
+
the publisher's DDL preview. */
|
|
469
|
+
async function phaseBDeps(argv, headers) {
|
|
470
|
+
const exec = (sql) => execPostgres(sql, argv["dbindex"]);
|
|
471
|
+
const execStatements = (statements) => execPostgresStatements(statements, argv["dbindex"]);
|
|
472
|
+
const draftRepo = createPgDraftRepo({ exec, execStatements });
|
|
473
|
+
const principal = principalForCliScript(headers, { mode: argv["principalMode"] || "observe" });
|
|
474
|
+
const { buildDomainDdlPreview } = await import("../engine/domain/appConfig.js");
|
|
475
|
+
return {
|
|
476
|
+
draftRepo,
|
|
477
|
+
gapRepo: createGapRepo({ exec }),
|
|
478
|
+
traceRepo: createTraceRepo({ exec }),
|
|
479
|
+
openDraft: (kind, name) => runDraftAction({ repo: draftRepo, principal, isPostgres: true, action: "open", args: { kind, name } }),
|
|
480
|
+
execStatements,
|
|
481
|
+
ddlWarnings: (content) => buildDomainDdlPreview(JSON.stringify(content), argv["dbindex"]).warnings ?? [],
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/* Doc 1 step 4d — what a derive turn reads: the configs as they are now (the OPEN draft, else published). */
|
|
486
|
+
function deriveDeps(argv) {
|
|
487
|
+
const draftRepo = createPgDraftRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) });
|
|
488
|
+
return { draftRepo, readConfig: createConfigReader(draftRepo) };
|
|
489
|
+
}
|
|
490
|
+
|
|
483
491
|
const handleCliCommand = async (data, cb, argv) => {
|
|
484
492
|
try {
|
|
485
493
|
const command = data.command.trim().toLowerCase();
|
|
@@ -673,6 +681,38 @@ const handleCliCommand = async (data, cb, argv) => {
|
|
|
673
681
|
* publish may). A refusal the user can act on is returned as `problems`, as companytheme
|
|
674
682
|
* does; `code` carries CC_DRAFT_STALE for a save made from an old revision.
|
|
675
683
|
*/
|
|
684
|
+
/*
|
|
685
|
+
* Doc 1 0b.6b — an AI turn from Control Center, through the central gateway
|
|
686
|
+
* (engine/cc/aiTurns.js). `send` answers at once; the turn finishes in the background, is
|
|
687
|
+
* recorded in SYS$CC_TURN with its cost, and the VERIFIED user is told {turnId, outcome}.
|
|
688
|
+
* The CLI holds no AI key: the gateway client signs with this company's biza.ai key.
|
|
689
|
+
*/
|
|
690
|
+
case "ccai": {
|
|
691
|
+
const aiResult = await runAiAction({
|
|
692
|
+
repo: createTurnRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) }),
|
|
693
|
+
/* Doc 1 step 2: an interview turn commits the process map. Step 4d: a derive turn commits it WITH its
|
|
694
|
+
trace links, in one transaction. */
|
|
695
|
+
mapRepo: createMapRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]), execStatements: (statements) => execPostgresStatements(statements, argv["dbindex"]) }),
|
|
696
|
+
/* Doc 1 step 4: an interview turn never removes what config implements. */
|
|
697
|
+
traceRepo: createTraceRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) }),
|
|
698
|
+
/* Doc 1 step 3: a Phase B turn writes drafts, gaps and trace links in ONE transaction. */
|
|
699
|
+
config: ["generate", "refine"].includes(data.action) ? await phaseBDeps(argv, data.headers) : data.action === "derive" ? deriveDeps(argv) : undefined,
|
|
700
|
+
gateway: createAiGatewayClient({ baseUrl: argv["aiGateway"] || argv["server"], subdomain: argv["subdomain"] }),
|
|
701
|
+
principal: principalForCliScript(data.headers, { mode: argv["principalMode"] || "observe" }),
|
|
702
|
+
isPostgres: isPostgresIndex(argv["dbindex"]),
|
|
703
|
+
action: data.action,
|
|
704
|
+
args: data.args,
|
|
705
|
+
notify: (userId, eventName, payload) => ClientEmitter.emitUsers([userId], eventName, payload),
|
|
706
|
+
logger,
|
|
707
|
+
});
|
|
708
|
+
cb(null, {
|
|
709
|
+
success: aiResult.ok,
|
|
710
|
+
...(aiResult.code ? { code: aiResult.code } : {}),
|
|
711
|
+
problems: aiResult.problems ?? [],
|
|
712
|
+
data: aiResult.data ?? null,
|
|
713
|
+
});
|
|
714
|
+
break;
|
|
715
|
+
}
|
|
676
716
|
case "ccdraft": {
|
|
677
717
|
const ccResult = await runDraftAction({
|
|
678
718
|
repo: createPgDraftRepo({
|
|
@@ -695,6 +735,31 @@ const handleCliCommand = async (data, cb, argv) => {
|
|
|
695
735
|
});
|
|
696
736
|
break;
|
|
697
737
|
}
|
|
738
|
+
/*
|
|
739
|
+
* Doc 1 step 1 — the Business Process Map behind Process Studio (engine/cc/mapStore.js):
|
|
740
|
+
* get / versions / save. A save is a new SYS$CC_PROCESS_MAP version, merged per element.
|
|
741
|
+
*/
|
|
742
|
+
case "ccmap": {
|
|
743
|
+
const mapResult = await runMapAction({
|
|
744
|
+
repo: createMapRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) }),
|
|
745
|
+
gapRepo: createGapRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) }),
|
|
746
|
+
/* Doc 1 step 4: the trace panel — the links, the turns that told the story, the config they point at. */
|
|
747
|
+
traceRepo: createTraceRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) }),
|
|
748
|
+
turnRepo: createTurnRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) }),
|
|
749
|
+
readConfig: createConfigReader(createPgDraftRepo({ exec: (sql) => execPostgres(sql, argv["dbindex"]) })),
|
|
750
|
+
principal: principalForCliScript(data.headers, { mode: argv["principalMode"] || "observe" }),
|
|
751
|
+
isPostgres: isPostgresIndex(argv["dbindex"]),
|
|
752
|
+
action: data.action,
|
|
753
|
+
args: data.args,
|
|
754
|
+
});
|
|
755
|
+
cb(null, {
|
|
756
|
+
success: mapResult.ok,
|
|
757
|
+
...(mapResult.code ? { code: mapResult.code } : {}),
|
|
758
|
+
problems: mapResult.problems ?? [],
|
|
759
|
+
data: mapResult.data ?? null,
|
|
760
|
+
});
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
698
763
|
case "runextcliscript": {
|
|
699
764
|
// External-access op invoker — enforces the capability boundary (dispatch.js),
|
|
700
765
|
// unlike raw `runcliscript`. Safe to expose: no valid token -> UNAUTHENTICATED.
|
package/bin/tunnelTarget.js
CHANGED
|
@@ -11,8 +11,35 @@
|
|
|
11
11
|
* database index, as it is everywhere else in this port.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
/*
|
|
15
|
+
* ⚠️ Doc 6 on Firebird (found live 2026-10-01): the LOGIN of a Firebird tenant went straight to FINA
|
|
16
|
+
* through this tunnel, so the CLI never saw FINA's answer and never added the signed principal — and
|
|
17
|
+
* every principal-guarded command (company theme, logo, user preferences) refused on Firebird. The login
|
|
18
|
+
* is now looped back to the CLI's own endpoint too (finaEndpoint.js forwards it to FINA and appends the
|
|
19
|
+
* principal); every other Firebird request still goes straight to FINA, byte for byte.
|
|
20
|
+
*
|
|
21
|
+
* Recognised by its REQUEST LINE only (method + path), never by anything in a body.
|
|
22
|
+
*/
|
|
23
|
+
const LOGIN_PATH = /%22get_valid_user_id%22|"get_valid_user_id"/i;
|
|
24
|
+
|
|
25
|
+
export const isLoginRequest = (firstChunk) => {
|
|
26
|
+
if (!firstChunk || firstChunk.length === 0) return false;
|
|
27
|
+
const head = Buffer.from(firstChunk).subarray(0, 2048).toString("latin1");
|
|
28
|
+
const lineEnd = head.indexOf("\r\n");
|
|
29
|
+
const requestLine = lineEnd === -1 ? head : head.slice(0, lineEnd);
|
|
30
|
+
const [method, path] = requestLine.split(" ");
|
|
31
|
+
return method === "POST" && typeof path === "string" && LOGIN_PATH.test(path);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const ownEndpoint = (argv) => ({
|
|
35
|
+
host: "127.0.0.1", // ourselves — never the public interface
|
|
36
|
+
port: argv.serverport,
|
|
37
|
+
secure: argv.isHttps === true, // how WE listen; argv.secure describes FINA
|
|
38
|
+
local: true,
|
|
39
|
+
});
|
|
40
|
+
|
|
14
41
|
/* Kept separate from hubEvent.js so it can be exercised without importing the socket machinery. */
|
|
15
|
-
export const resolveTunnelTarget = (argv, isPostgresIndex) => {
|
|
42
|
+
export const resolveTunnelTarget = (argv, isPostgresIndex, firstChunk) => {
|
|
16
43
|
const fina = {
|
|
17
44
|
host: argv.hostname,
|
|
18
45
|
port: argv.port,
|
|
@@ -20,6 +47,7 @@ export const resolveTunnelTarget = (argv, isPostgresIndex) => {
|
|
|
20
47
|
local: false,
|
|
21
48
|
};
|
|
22
49
|
|
|
50
|
+
if (isLoginRequest(firstChunk)) return ownEndpoint(argv);
|
|
23
51
|
if (!argv.dbindex) return fina;
|
|
24
52
|
|
|
25
53
|
let servedHere = false;
|
|
@@ -32,10 +60,35 @@ export const resolveTunnelTarget = (argv, isPostgresIndex) => {
|
|
|
32
60
|
}
|
|
33
61
|
if (!servedHere) return fina;
|
|
34
62
|
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
63
|
+
return ownEndpoint(argv);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/*
|
|
67
|
+
* One tunnelled request: biz-a's bytes arrive on `stream`; the connection is opened only once the first
|
|
68
|
+
* bytes say where it must go (resolveTunnelTarget), that first chunk is written first so nothing is
|
|
69
|
+
* lost, and the rest is piped. The answer flows back through `transform()` (the response-header
|
|
70
|
+
* injection). A connection that fails ends the stream, so biz-a is not left waiting.
|
|
71
|
+
*/
|
|
72
|
+
export const pipeTunnel = ({ stream, argv, isPostgresIndex, connect, transform, idleMs, onClient, logger = console }) => {
|
|
73
|
+
stream.once("data", (first) => {
|
|
74
|
+
stream.pause();
|
|
75
|
+
const target = resolveTunnelTarget(argv, isPostgresIndex, first);
|
|
76
|
+
const client = connect(target);
|
|
77
|
+
onClient?.(client);
|
|
78
|
+
if (idleMs) client.setTimeout(idleMs);
|
|
79
|
+
client.on("timeout", () => client.end());
|
|
80
|
+
client.on("connect", () => {
|
|
81
|
+
client.write(first);
|
|
82
|
+
stream.pipe(client).pipe(transform()).pipe(stream);
|
|
83
|
+
stream.resume();
|
|
84
|
+
});
|
|
85
|
+
client.on("error", (err) => {
|
|
86
|
+
logger.error?.(
|
|
87
|
+
`API Error : tunnel could not reach ${target.local ? "this CLI's own endpoint" : "the API"} at ${target.host}:${target.port} — `,
|
|
88
|
+
err,
|
|
89
|
+
);
|
|
90
|
+
stream.end();
|
|
91
|
+
});
|
|
92
|
+
stream.on("end", () => client.destroy());
|
|
93
|
+
});
|
|
41
94
|
};
|
|
@@ -218,14 +218,14 @@ export async function initiate(payload) {
|
|
|
218
218
|
current_state_code: def.initial_state_code,
|
|
219
219
|
status: "ACTIVE",
|
|
220
220
|
version_no: def.version,
|
|
221
|
-
created_by: userId
|
|
221
|
+
created_by: userId ?? null,
|
|
222
222
|
SYS$BPM_STATE_HISTORY: [
|
|
223
223
|
{
|
|
224
224
|
from_state_code: null,
|
|
225
225
|
to_state_code: def.initial_state_code,
|
|
226
226
|
trigger_type: "SYSTEM",
|
|
227
227
|
actor_type: "USER",
|
|
228
|
-
actor_id: userId
|
|
228
|
+
actor_id: userId ?? null,
|
|
229
229
|
request_id: `START_${entityType}_${entityId}_${new Date().getTime()}`,
|
|
230
230
|
},
|
|
231
231
|
],
|
|
@@ -773,7 +773,7 @@ export async function terminate(payload) {
|
|
|
773
773
|
to_state_code: currentStateCode,
|
|
774
774
|
trigger_type: "USER_EVENT",
|
|
775
775
|
actor_type: "USER",
|
|
776
|
-
actor_id: userId
|
|
776
|
+
actor_id: userId ?? null,
|
|
777
777
|
request_id: `TERMINATE_${instanceId}_${new Date().getTime()}`,
|
|
778
778
|
},
|
|
779
779
|
};
|