@unoverse-platform/studio 0.1.90 → 0.1.92

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 (132) hide show
  1. package/local/plugin.ts +21 -65
  2. package/package.json +13 -3
  3. package/vendor/base/src/agent-mcp/discovery.ts +203 -0
  4. package/vendor/base/src/agent-mcp/handoff.ts +56 -0
  5. package/vendor/base/src/agent-mcp/index.ts +44 -0
  6. package/vendor/base/src/agent-mcp/invoke.ts +153 -0
  7. package/vendor/base/src/agent-mcp/lean.ts +114 -0
  8. package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
  9. package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
  10. package/vendor/base/src/agent-mcp/rowComponents.ts +234 -0
  11. package/vendor/base/src/agent-mcp/tools.ts +134 -0
  12. package/vendor/base/src/agent-mcp/types.ts +138 -0
  13. package/vendor/base/src/auth/auth.ts +307 -0
  14. package/vendor/base/src/auth/authConfig.ts +73 -0
  15. package/vendor/base/src/boot.ts +245 -0
  16. package/vendor/base/src/definitions/definitions.ts +18 -0
  17. package/vendor/base/src/definitions/dsPackage.ts +45 -0
  18. package/vendor/base/src/definitions/fsCache.ts +155 -0
  19. package/vendor/base/src/definitions/index.ts +19 -0
  20. package/vendor/base/src/definitions/inputs.ts +30 -0
  21. package/vendor/base/src/definitions/load/compile/component.ts +80 -0
  22. package/vendor/base/src/definitions/load/compile/index.ts +12 -0
  23. package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
  24. package/vendor/base/src/definitions/load/compile/template.ts +153 -0
  25. package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
  26. package/vendor/base/src/definitions/load/expand/index.ts +17 -0
  27. package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
  28. package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
  29. package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
  30. package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
  31. package/vendor/base/src/definitions/load/load.ts +487 -0
  32. package/vendor/base/src/definitions/model.ts +238 -0
  33. package/vendor/base/src/definitions/registry/catalog.ts +659 -0
  34. package/vendor/base/src/definitions/registry/homes.ts +252 -0
  35. package/vendor/base/src/definitions/theme.ts +383 -0
  36. package/vendor/base/src/executor.ts +284 -0
  37. package/vendor/base/src/http/uwsFetchAdapter.ts +397 -0
  38. package/vendor/base/src/items/baseVersion.ts +96 -0
  39. package/vendor/base/src/items/collect.ts +246 -0
  40. package/vendor/base/src/items/fingerprint.ts +34 -0
  41. package/vendor/base/src/items/publish.ts +212 -0
  42. package/vendor/base/src/lint/context.mjs +56 -0
  43. package/vendor/base/src/lint/design/defs.mjs +46 -0
  44. package/vendor/base/src/lint/design/file.mjs +737 -0
  45. package/vendor/base/src/lint/design/index.d.mts +30 -0
  46. package/vendor/base/src/lint/design/index.mjs +996 -0
  47. package/vendor/base/src/lint/design/tokens.mjs +128 -0
  48. package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
  49. package/vendor/base/src/lint/design/walk.mjs +444 -0
  50. package/vendor/base/src/lint/index.mjs +28 -0
  51. package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
  52. package/vendor/base/src/lint/nodes/index.d.mts +28 -0
  53. package/vendor/base/src/lint/nodes/index.mjs +82 -0
  54. package/vendor/base/src/lint/nodes/load.mjs +126 -0
  55. package/vendor/base/src/lint/nodes/node.mjs +764 -0
  56. package/vendor/base/src/lint/nodes/package.mjs +112 -0
  57. package/vendor/base/src/lint/nodes/schema.mjs +75 -0
  58. package/vendor/base/src/logger.ts +33 -0
  59. package/vendor/base/src/manifests/compose.ts +539 -0
  60. package/vendor/base/src/manifests/executor/callback.ts +107 -0
  61. package/vendor/base/src/manifests/executor/context.ts +110 -0
  62. package/vendor/base/src/manifests/executor/index.ts +47 -0
  63. package/vendor/base/src/manifests/executor/lookup.ts +21 -0
  64. package/vendor/base/src/manifests/executor/memory.ts +62 -0
  65. package/vendor/base/src/manifests/executor/promise.ts +71 -0
  66. package/vendor/base/src/manifests/executor/refire.ts +34 -0
  67. package/vendor/base/src/manifests/executor/send.ts +65 -0
  68. package/vendor/base/src/manifests/executor/session.ts +46 -0
  69. package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
  70. package/vendor/base/src/manifests/index.ts +280 -0
  71. package/vendor/base/src/manifests/integrity.ts +93 -0
  72. package/vendor/base/src/manifests/rows.ts +33 -0
  73. package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
  74. package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
  75. package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
  76. package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
  77. package/vendor/base/src/manifests/runtime/channels/service.ts +138 -0
  78. package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
  79. package/vendor/base/src/manifests/runtime/context.ts +156 -0
  80. package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
  81. package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
  82. package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
  83. package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
  84. package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
  85. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
  86. package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
  87. package/vendor/base/src/manifests/runtime/events.ts +223 -0
  88. package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
  89. package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
  90. package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
  91. package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
  92. package/vendor/base/src/manifests/runtime/index.ts +57 -0
  93. package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
  94. package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
  95. package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
  96. package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
  97. package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
  98. package/vendor/base/src/manifests/runtime/state.ts +171 -0
  99. package/vendor/base/src/manifests/runtime/templating.ts +110 -0
  100. package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
  101. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +550 -0
  102. package/vendor/base/src/manifests/runtime/usage.ts +87 -0
  103. package/vendor/base/src/manifests/source.ts +196 -0
  104. package/vendor/base/src/manifests/watch.ts +67 -0
  105. package/vendor/base/src/markdown/document.ts +141 -0
  106. package/vendor/base/src/paths.ts +180 -0
  107. package/vendor/base/src/platform/audioManager.ts +100 -0
  108. package/vendor/base/src/platform/clientTransport.ts +65 -0
  109. package/vendor/base/src/platform/executionContext.ts +157 -0
  110. package/vendor/base/src/platform/index.ts +10 -0
  111. package/vendor/base/src/platform/pluginAPI.ts +146 -0
  112. package/vendor/base/src/platform/redis.ts +54 -0
  113. package/vendor/base/src/platform/serviceCalls.ts +222 -0
  114. package/vendor/base/src/pluginBase.ts +248 -0
  115. package/vendor/base/src/plugins/README.md +57 -0
  116. package/vendor/base/src/plugins/discovery.ts +87 -0
  117. package/vendor/base/src/plugins/index.ts +16 -0
  118. package/vendor/base/src/plugins/loader.ts +179 -0
  119. package/vendor/base/src/plugins/redis.ts +36 -0
  120. package/vendor/base/src/plugins/state.ts +53 -0
  121. package/vendor/base/src/registry.ts +95 -0
  122. package/vendor/base/src/security/permissions.ts +93 -0
  123. package/vendor/base/src/security/rules.ts +73 -0
  124. package/vendor/base/src/template/SafeExpression.ts +556 -0
  125. package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
  126. package/vendor/base/src/template/index.ts +22 -0
  127. package/vendor/base/src/types.ts +50 -0
  128. package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
  129. package/vendor/base/src/workflow/PlatformMemoryCore.ts +449 -0
  130. package/vendor/base/src/workflow/routing.ts +122 -0
  131. package/vendor/sdk/lib/core/store.ts +26 -6
  132. package/local/mcp.ts +0 -265
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Build what a manifest's templates and expressions may see.
3
+ *
4
+ * `config` arrives ALREADY RESOLVED: the engine runs its template resolver before it
5
+ * calls execute, so {{signal.x}} and {{prompt.y}} an author wrote in a config field are
6
+ * plain text by the time they reach here.
7
+ *
8
+ * `executionContext.auth.accessToken` — the CALLER'S JWT (AUTH_TOKEN_FLOW.md) — is
9
+ * DELIBERATELY WITHHELD, and must stay that way. A code node may reach it because a code
10
+ * node is bounded by provenance; a manifest is data that can arrive by paste or database
11
+ * row. The blast radii are not comparable: a third-party API key reaches that vendor
12
+ * account, while a platform JWT IS the user against our own services, so a pasted
13
+ * manifest could forward it to any host its allowedHosts list allows.
14
+ *
15
+ * `auth.user` — the signed-in person's email, id and name — IS exposed, as `user`. That
16
+ * is not a softening of the rule, it is the rule applied to a different thing: an email
17
+ * authenticates nothing, and it is the join key every CRM, support and account node needs.
18
+ * Withholding it would force those nodes to take identity off the WIRE, which is strictly
19
+ * worse: a caller could then ask for someone else's record. Never widen this to the token.
20
+ *
21
+ * The tool bridge DOES use the token, and that is not a contradiction: there the
22
+ * platform is calling its own internal /mcp on the user's behalf, rather than data
23
+ * deciding where a secret goes.
24
+ *
25
+ * EXPORTED ONLY FOR THE SIBLING FILES IN THIS FOLDER (the two executors and the re-fire).
26
+ * Nothing outside `executor/` may import it: every consumer of a RunContext outside this
27
+ * folder receives one already built, and a second builder is how the token rule would
28
+ * eventually be widened by accident. Pinned by manifest-chain.test.ts.
29
+ */
30
+ import type { ComposedNode } from "../compose.js";
31
+ import { applyResolvers, emptyContext, type RunContext } from "../runtime/index.js";
32
+
33
+ export function contextFor(node: ComposedNode, inputs: any, config: any, executionContext: any): RunContext {
34
+ const user = executionContext?.auth?.user ?? {};
35
+ return emptyContext({
36
+ config: applyResolvers(config ?? {}, node),
37
+ // The bag holds EVERY credential in the workflow keyed by name. Selecting by name is
38
+ // the manifest's job, which is what stops a node authenticating with a neighbour's
39
+ // apiKey (04-credentials.md).
40
+ credentials: executionContext?.credentials ?? {},
41
+ signal: inputs ?? {},
42
+ services: executionContext?.services ?? {},
43
+ // The caller's disconnect. A tool loop stops between turns when it fires, so a run
44
+ // the engine reaped cannot keep spending model calls (observed live 2026-09-05).
45
+ abort: executionContext?.abortSignal,
46
+ // Picked field by field, never spread: a future field on auth.user must be an explicit
47
+ // decision to expose, not something a manifest silently inherits.
48
+ user: { email: user.email, id: user.id ?? user.sub, name: user.name },
49
+ // The PLATFORM's ids, which is what state keys are built from. Same precedence the
50
+ // retired CRM code used, and it is not interchangeable with user.id above.
51
+ //
52
+ // `conversationId` and `chatId` are here because the AUDIO LANE is keyed by conversation.
53
+ // Their absence was a silent, total failure of the voice node: `runDuplexSession` fell back
54
+ // to the node TYPE as its key, so `setAudioDataHandler` compared the client's real session id
55
+ // against the string "OpenAIRealtimeVoice", dropped every microphone frame, and sent the
56
+ // model's audio to a conversation nobody was listening on. No error anywhere — just silence.
57
+ //
58
+ // Same precedence the retired node used: publishingContext first, then workflow variables.
59
+ scope: {
60
+ userId: executionContext?.publishingContext?.userId ?? executionContext?.workflow?.variables?.userId,
61
+ workflowId: executionContext?.workflowId ?? executionContext?.workflow?.id,
62
+ conversationId:
63
+ executionContext?.publishingContext?.conversationId ?? executionContext?.workflow?.variables?.conversationId,
64
+ chatId: executionContext?.publishingContext?.chatId ?? executionContext?.workflow?.variables?.chatId,
65
+ executionId: executionContext?.executionId,
66
+ // Which INSTANCE on the canvas. The retired Code node fell back to the literal "code" when
67
+ // this was missing, which meant two Code nodes minted the same universal id and the saved
68
+ // context stored them under one key. Passing it through rather than defaulting is what makes
69
+ // that failure impossible instead of merely unlikely.
70
+ nodeId: executionContext?.nodeId,
71
+ /**
72
+ * THE PLATFORM'S OWN API, for a node that calls us rather than a vendor.
73
+ *
74
+ * DERIVED, not configured. A node runs INSIDE the unoverse service, so it is calling itself —
75
+ * asking a developer to supply the platform's own address would be configuration for something
76
+ * the process already knows. `UNOVERSE_RUNTIME_PORT` is the internal listener and is already set
77
+ * in docker-compose; the server reads it the same way (`RUNTIME_PORT`), and already builds its own
78
+ * loopback url as `http://127.0.0.1:${port}` elsewhere.
79
+ *
80
+ * LOOPBACK, not a service name: :4106 is the UNGATED internal listener, published to 127.0.0.1
81
+ * only and never widened. Reaching it from anywhere else would be a mistake, so the address says
82
+ * so.
83
+ *
84
+ * The retired SpatialIngest read UNOVERSE_SERVICE_URL with a `|| "http://localhost:4106"`
85
+ * fallback — a variable that is set NOWHERE in this repo, so the fallback was the whole
86
+ * behaviour. Deriving it removes both the dead variable and the hardcoded string.
87
+ */
88
+ platformUrl: `http://127.0.0.1:${process.env.UNOVERSE_RUNTIME_PORT ?? 4106}`,
89
+ /**
90
+ * THE ENGINE, on :4101, and it is NOT the same service as `platformUrl`.
91
+ *
92
+ * Two internal listeners, two different sets of routes, and picking the wrong one is a
93
+ * 404 rather than anything that reads like an address problem:
94
+ *
95
+ * :4106 the unoverse runtime — /service-call, /execute, /skills
96
+ * :4101 the engine — /spatial/search, /dictionary/entry, /workflows
97
+ *
98
+ * SpatialSearch needs BOTH, which is what makes the distinction worth a second field
99
+ * rather than a note: its searches are engine routes and its skill reads are runtime
100
+ * routes. The retired node had the same split and hid it in two different env vars
101
+ * (WORKFLOW_SERVICE_URL and UNOVERSE_SERVICE_URL), neither of which is set anywhere in
102
+ * this repo, so both fallbacks were the entire behaviour.
103
+ *
104
+ * Same derivation as above, and the same reasoning: loopback because these listeners
105
+ * are internal and never published, and derived so there is no dead variable to set.
106
+ */
107
+ engineUrl: `http://127.0.0.1:${process.env.ENGINE_PORT ?? process.env.WORKFLOW_SERVICE_PORT ?? 4101}`,
108
+ },
109
+ });
110
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The TWO executors every manifest node shares.
3
+ *
4
+ * There are exactly two, not one per node. A node's identity, ports, config and
5
+ * selection text are DATA in the registry; the code that performs it is one of these
6
+ * classes. Which one is decided by `kind`, and the split exists only because the
7
+ * platform genuinely has two execution shapes: settle-once (PromiseNode) and
8
+ * emit-repeatedly (CallbackNode). The registry itself derives executionMode by
9
+ * checking whether the prototype has initializeState, so the shapes cannot be merged.
10
+ *
11
+ * They are STATELESS with respect to which node they are running. The manifest is
12
+ * looked up by nodeType at execution time rather than closed over at registration
13
+ * (lookup.ts), which is what keeps this at two classes instead of one per manifest. It
14
+ * also means a reload swaps behaviour with no re-registration: the class stays, the
15
+ * data moves.
16
+ *
17
+ * ONE FILE PER JOB:
18
+ *
19
+ * lookup.ts how an executor finds its manifest by node type
20
+ * context.ts what a MANIFEST may see — and the token it must never see
21
+ * session.ts what the executor hands to PLATFORM code (session, state, audio)
22
+ * toolBridge.ts the agent harness, bridged into the tool loop
23
+ * refire.ts the hybrid contract: a doc mutation re-fires the workflow channel
24
+ * promise.ts settle-once
25
+ * callback.ts emit-repeatedly
26
+ *
27
+ * `contextFor` is deliberately NOT re-exported here: everything outside this folder
28
+ * receives a RunContext already built, and a second builder is how the token rule would
29
+ * eventually be widened by accident.
30
+ */
31
+ import type { ComposedNode } from "../compose.js";
32
+ import { ManifestPromiseExecutor } from "./promise.js";
33
+ import { ManifestCallbackExecutor } from "./callback.js";
34
+
35
+ export { setManifestLookup } from "./lookup.js";
36
+ export { ManifestPromiseExecutor } from "./promise.js";
37
+ export { ManifestCallbackExecutor } from "./callback.js";
38
+
39
+ /**
40
+ * The class a node of this kind registers.
41
+ *
42
+ * Returns one of the two shared classes. The registry stores the class and
43
+ * constructs it with the node type, so nothing here is per-manifest.
44
+ */
45
+ export function executorForKind(kind: ComposedNode["kind"]): any {
46
+ return kind === "CallbackNode" ? ManifestCallbackExecutor : ManifestPromiseExecutor;
47
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * HOW AN EXECUTOR FINDS ITS MANIFEST. The two executor classes are stateless with respect
3
+ * to which node they are running: the manifest is looked up by nodeType at execution time
4
+ * rather than closed over at registration, which is what keeps the platform at two classes
5
+ * instead of one per manifest — and what makes a reload swap behaviour with no
6
+ * re-registration: the class stays, the data moves.
7
+ */
8
+ import type { ComposedNode } from "../compose.js";
9
+
10
+ /** Set by the loader so both executors can resolve their manifest by node type. */
11
+ let lookup: (type: string) => ComposedNode | undefined = () => undefined;
12
+
13
+ export function setManifestLookup(fn: (type: string) => ComposedNode | undefined): void {
14
+ lookup = fn;
15
+ }
16
+
17
+ export function manifestFor(type: string): ComposedNode {
18
+ const node = lookup(type);
19
+ if (!node) throw new Error(`No manifest loaded for node type "${type}" — it may have been unloaded mid-run.`);
20
+ return node;
21
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * THE MEMORY LANES — the two harness calls every legacy agent made and the manifest
3
+ * executor stopped making, which is how the memory system sat healthy-but-idle after the
4
+ * migration: a running memory server, an empty ingest stream, and toggles that had
5
+ * vanished from the UI because no manifest declared them.
6
+ *
7
+ * Wired at the EXECUTOR, not in any manifest, mirroring how the engine injects the
8
+ * goal-memory toolset from the same flags (PlatformMemoryCore): a node declares
9
+ * `enableUserMemory` / `enableAgentMemory` in its config and the platform does the rest.
10
+ * Both lanes self-gate on the toggle and on identity, so a node without the fields, a
11
+ * bench run, or a headless caller costs nothing.
12
+ *
13
+ * The implementations live in ../../agent-mcp (memoryContext.ts / memoryIngest.ts) —
14
+ * they moved there in the harness extraction and are TRANSCRIBED conventions:
15
+ * hydration prepends the user's context snapshot to the USER PROMPT (never the system
16
+ * prompt — facts that ride the request refresh every turn and survive prompt caching),
17
+ * and ingest saves original input + final answer, never tool chatter.
18
+ */
19
+
20
+ /** Prepend the user's memory context to config.prompt when the toggle is on. */
21
+ export async function hydrateMemory(config: any, executionContext: any): Promise<any> {
22
+ if (config?.enableUserMemory !== true || typeof config?.prompt !== "string") return config;
23
+ try {
24
+ const harness: any = await import("../../agent-mcp/index.js");
25
+ const prompt = await harness.hydratePromptWithUserMemory(config, executionContext, console, config.prompt);
26
+ return prompt === config.prompt ? config : { ...config, prompt };
27
+ } catch (e: any) {
28
+ // Non-blocking, exactly as the legacy hydration was: memory enriches a run, it
29
+ // never gates one.
30
+ console.log(`[manifests] user-memory hydration failed (non-blocking): ${e?.message ?? e}`);
31
+ return config;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Save the completed turn (original input + final answer) to the memory server.
37
+ * Fire-and-forget; `ingestConversationTurn` itself gates on the toggle and identity.
38
+ * The answer is read off the node's own outputs — `text` is the settled connector on
39
+ * every agent family (the streamed `stream` connector carries the same final value).
40
+ */
41
+ export async function ingestMemoryTurn(
42
+ nodeType: string,
43
+ config: any,
44
+ executionContext: any,
45
+ outputs: Record<string, unknown>,
46
+ ): Promise<void> {
47
+ if (config?.enableUserMemory !== true) return;
48
+ const answer = outputs?.text ?? outputs?.stream ?? outputs?.output;
49
+ if (typeof answer !== "string" || !answer) return;
50
+ try {
51
+ const harness: any = await import("../../agent-mcp/index.js");
52
+ harness.ingestConversationTurn(
53
+ config,
54
+ executionContext,
55
+ { userMessage: String(config?.prompt ?? ""), agentResponse: answer },
56
+ console,
57
+ nodeType,
58
+ );
59
+ } catch (e: any) {
60
+ console.log(`[manifests] memory ingest failed (non-blocking): ${e?.message ?? e}`);
61
+ }
62
+ }
@@ -0,0 +1,71 @@
1
+ /** Settle-once. One request, one result. */
2
+ import { performApi, performService } from "../runtime/index.js";
3
+ import { deliverSends } from "./send.js";
4
+ import { assertAuthorized } from "../runtime/authorize.js";
5
+ import { manifestFor } from "./lookup.js";
6
+ import { contextFor } from "./context.js";
7
+ import { sessionFor, stateStoreFor } from "./session.js";
8
+ import { refireAfterDocMutation } from "./refire.js";
9
+ import { toolBridgeFor } from "./toolBridge.js";
10
+
11
+ export class ManifestPromiseExecutor {
12
+ logger: any = console;
13
+ constructor(readonly nodeType: string) {}
14
+
15
+ /**
16
+ * The SERVICE channel: a method another node called over a service edge.
17
+ *
18
+ * Independent of the workflow channel. A service call returns a value to the caller
19
+ * and does NOT fire this node's output connectors; a graph trigger fires the outputs
20
+ * and does not answer a caller. The two never cross (08-mcp-services.md).
21
+ */
22
+ async handleServiceCall(method: string, params: any, config: any, executionContext: any): Promise<unknown> {
23
+ const node = manifestFor(this.nodeType);
24
+ // The service channel is gated too, and it is the one that would be missed. A node
25
+ // reached over a service edge never fires its own connectors, so it is easy to think of
26
+ // as an internal detail of the caller. It is not: it runs this node's calls with this
27
+ // node's credentials, and leaving it open would make a service edge the way around a
28
+ // role rather than a way to reuse a capability.
29
+ assertAuthorized(node, executionContext, config);
30
+ const result = await performService(
31
+ node,
32
+ method,
33
+ params ?? {},
34
+ contextFor(node, {}, config, executionContext),
35
+ stateStoreFor(executionContext),
36
+ // The caller's live session, for a method that renders content cards. Assembled HERE
37
+ // because the token is deliberately absent from the manifest's context and must stay
38
+ // that way: the executor holds it and hands it to platform code, never to the manifest.
39
+ sessionFor(executionContext),
40
+ );
41
+ await refireAfterDocMutation(node, method, config, executionContext);
42
+ return result;
43
+ }
44
+
45
+ async execute(inputs: any, config: any, executionContext: any): Promise<{ __outputs: Record<string, unknown> }> {
46
+ const node = manifestFor(this.nodeType);
47
+ // BEFORE anything is built or sent, so a refused run costs no vendor request, no token,
48
+ // and no side effect. Authorization that runs after the call has gone out is an audit
49
+ // log, not a gate.
50
+ assertAuthorized(node, executionContext, config);
51
+ const { outputs, emissions } = await performApi(
52
+ node,
53
+ contextFor(node, inputs, config, executionContext),
54
+ () => {},
55
+ node.api?.toolExchange ? await toolBridgeFor(executionContext) : undefined,
56
+ stateStoreFor(executionContext),
57
+ // No audio lane on the promise path (a duplex session is always a CallbackNode).
58
+ undefined,
59
+ // The caller's live session, for a node that publishes template data. Assembled here
60
+ // for the same reason as the service channel's: the executor holds the identity and
61
+ // hands it to platform code, never to the manifest.
62
+ sessionFor(executionContext),
63
+ );
64
+ // Any `send` row addressed a NODE rather than a connector: deliver those before settling,
65
+ // so a loop's next pass is already queued when this node reports done.
66
+ await deliverSends(node, emissions, executionContext);
67
+ // Outputs go back TOP-LEVEL under __outputs; anything else never reaches a
68
+ // downstream node.
69
+ return { __outputs: outputs };
70
+ }
71
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * THE HYBRID CONTRACT (Pattern C, 08-mcp-services.md): a service method that MUTATED the
3
+ * node's document re-fires the WORKFLOW channel, so a downstream renderer receives the
4
+ * fresh markdown without the agent doing anything. Read methods must not re-fire — the
5
+ * retired SmartDocument learned that live, when every outline() re-emitted the whole
6
+ * document and flooded the renderer.
7
+ *
8
+ * Detection is by the method's own call list naming a mutating docstore op, so the manifest
9
+ * cannot promise the contract and forget it: declaring the op IS declaring the re-fire.
10
+ *
11
+ * `executeNodeWithRouting` is the engine's own bridge (NodeExecutionUtils), reached through
12
+ * the plugin api exactly as the retired executor reached it via getPlatform(). Absent
13
+ * (tests, headless) the mutation still returns to the caller — the render is a side channel
14
+ * and failures here are logged, never thrown, exactly as the retired node treated them.
15
+ */
16
+ import { performApi, MUTATING_DOC_OPS } from "../runtime/index.js";
17
+ import { contextFor } from "./context.js";
18
+ import { stateStoreFor } from "./session.js";
19
+
20
+ export async function refireAfterDocMutation(node: any, method: string, config: any, executionContext: any): Promise<void> {
21
+ const calls = node.api?.service?.[method]?.calls ?? [];
22
+ if (!calls.some((c: any) => c.docstore && MUTATING_DOC_OPS.has(String(c.docstore)))) return;
23
+ const route = executionContext?.api?.executeNodeWithRouting;
24
+ if (typeof route !== "function") return;
25
+ const run = async (_inputs: any, cfg: any, ec: any) => {
26
+ const { outputs } = await performApi(node, contextFor(node, {}, cfg, ec), () => {}, undefined, stateStoreFor(ec));
27
+ return { __outputs: outputs };
28
+ };
29
+ try {
30
+ await route(run, {}, config, executionContext);
31
+ } catch (e: any) {
32
+ console.log(`[manifests] ${node.type}: post-mutation re-fire failed — ${e?.message ?? e}`);
33
+ }
34
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * `send` rows — one node handing a payload to a NAMED node, with no connector and no edge.
3
+ *
4
+ * WHY THIS EXISTS. LoopEnd already names its partner in `loopStartNodeId`. Requiring an edge as
5
+ * well made the same fact true in two places, and the two could disagree: the canonical loop
6
+ * pattern shipped without the edge for months, which builds a loop that runs exactly one pass and
7
+ * looks like it worked. A row that addresses a node removes the second place.
8
+ *
9
+ * It is deliberately NOT loop-specific. "Deliver this to that node" is the general primitive;
10
+ * the loop is its first caller.
11
+ *
12
+ * DELIVERY HAPPENS HERE, at the executor boundary, and not in events.ts. The events table
13
+ * DESCRIBES what leaves a node; performing a side effect needs the execution context, which only
14
+ * an executor holds. Keeping the split means a manifest still cannot reach the engine directly.
15
+ *
16
+ * FAILURE IS LOUD. A send that silently does nothing is a loop that stops after one pass while
17
+ * reporting success, which is the exact failure this replaced. An unresolvable target or an
18
+ * absent host raises, in the same spirit as `loop: advance` raising "loop state not found"
19
+ * rather than returning done.
20
+ */
21
+ import type { Emission } from "../runtime/http/response.js";
22
+
23
+ /**
24
+ * The host's delivery bridge. Absent in tests and headless runs.
25
+ *
26
+ * SAME RUN ONLY, and that is the point of the leading `executionId`: it is the SENDER's own,
27
+ * taken from its execution context, so a node can only ever reach nodes in the workflow it is
28
+ * running in. There is no addressing scheme that reaches another execution.
29
+ */
30
+ type DeliverToNode = (
31
+ executionId: string,
32
+ targetNodeId: string,
33
+ handle: string,
34
+ value: unknown,
35
+ ) => Promise<void> | void;
36
+
37
+ /**
38
+ * Deliver every `send` emission this run produced. A no-op for a node that declares none, which
39
+ * is every node but LoopEnd today, so this costs nothing on the common path.
40
+ */
41
+ export async function deliverSends(node: any, emissions: Emission[], executionContext: any): Promise<void> {
42
+ const sends = emissions.filter((e) => e.to);
43
+ if (sends.length === 0) return;
44
+
45
+ const deliver: DeliverToNode | undefined = executionContext?.api?.deliverToNode;
46
+ if (typeof deliver !== "function") {
47
+ throw new Error(
48
+ `${node?.type ?? "node"}: an events row sends to another node, but this runtime provides no ` +
49
+ `deliverToNode bridge, so the message would be dropped. The run is stopped rather than ` +
50
+ `continuing with a message nobody received.`,
51
+ );
52
+ }
53
+
54
+ const executionId = executionContext?.executionId ?? executionContext?.scope?.executionId;
55
+ if (!executionId) {
56
+ throw new Error(
57
+ `${node?.type ?? "node"}: an events row sends to another node, but this run has no executionId, ` +
58
+ `so the delivery could not be confined to this workflow.`,
59
+ );
60
+ }
61
+
62
+ for (const s of sends) {
63
+ await deliver(String(executionId), String(s.to), s.handle ?? "input", s.value);
64
+ }
65
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * WHAT THE EXECUTOR ASSEMBLES FROM THE EXECUTION CONTEXT and hands to PLATFORM code —
3
+ * never to the manifest. `context.ts` builds what a manifest may READ; these build what
4
+ * the runtime's own capabilities need, and the difference is exactly the token rule:
5
+ * `sessionFor` carries the caller's JWT because the platform calling its own surface on
6
+ * the user's behalf is not data deciding where a secret goes.
7
+ */
8
+ import { makeStateStore } from "../runtime/index.js";
9
+
10
+ /**
11
+ * WHO IS WATCHING, for a service method that renders content cards onto a live screen or
12
+ * a node that publishes template data.
13
+ *
14
+ * Kept out of `contextFor` on purpose (§9.4): a manifest that could read the token could
15
+ * forward it anywhere its allowedHosts allows.
16
+ *
17
+ * `publishingContext` is the trigger-supplied identity of the conversation the run belongs
18
+ * to. Absent for builder, test and headless callers, which is exactly when card rendering
19
+ * should no-op rather than needing a flag.
20
+ */
21
+ export function sessionFor(executionContext: any) {
22
+ const pub = executionContext?.publishingContext;
23
+ return {
24
+ userId: pub?.userId,
25
+ conversationId: pub?.conversationId,
26
+ chatId: pub?.chatId,
27
+ accessToken: executionContext?.auth?.accessToken,
28
+ };
29
+ }
30
+
31
+ /**
32
+ * The platform's audio lane, through the same handle a code node already gets.
33
+ *
34
+ * `null` when the platform has no lane, and that is NOT an error: a voice node with nowhere to
35
+ * send audio still holds a valid conversation with the vendor, and its transcripts still reach
36
+ * the workflow over the events table. Only the audio is lost, which is the correct degradation
37
+ * for a headless run.
38
+ */
39
+ export function audioLaneFor(executionContext: any) {
40
+ return executionContext?.api?.getAudioWebSocketManager?.() ?? null;
41
+ }
42
+
43
+ /** Redis, through the handle the plugin library already provides to a code node. */
44
+ export function stateStoreFor(executionContext: any) {
45
+ return makeStateStore(executionContext?.api?.getRedisClient?.() ?? null, process.env.REDIS_NAMESPACE);
46
+ }