@parall/parel-channel 1.42.0 → 1.43.0

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.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Channel-capability credential injection — the parel-native form of the
3
+ * feishu-cli broker (multi-channel-architecture-design §8.3).
4
+ *
5
+ * Bridges deliver the capability as a PATH pointer shim that mints a fresh
6
+ * short-lived tenant token per lark-cli invocation. In parel we control
7
+ * neither the sandbox PATH nor a bridge process — but we DO control the
8
+ * per-turn invocation context, which sandbox-e2b flattens into every exec's
9
+ * env. So the connector mints at emit time and injects the official
10
+ * LARKSUITE_CLI_* env set: bare `lark-cli` works ambiently for the whole
11
+ * turn (turn length ≪ token TTL ~2h), the app secret never leaves the
12
+ * platform, and revocation bites on the next emit's mint (403 → no env).
13
+ *
14
+ * The mint endpoint IS the gate (the platform re-evaluates connection state
15
+ * + flag on every request — the no-drift invariant), so the connector needs
16
+ * no declaration-plane fetch: a 403 is the revocation signal and is
17
+ * negative-cached briefly to keep a revoked grant from adding one 403 per
18
+ * turn.
19
+ */
20
+ import type { ConnectorContext } from '@parel/plugin-sdk';
21
+ /** Test hook: the cache is module-level state, so suites reset it between cases. */
22
+ export declare function resetChannelCapabilityCache(): void;
23
+ /**
24
+ * Returns the feishu-cli env set to spread into an envelope's invocation
25
+ * context, or null when the capability is not granted (or the mint degraded
26
+ * — a transient platform error must never block a dispatch emit).
27
+ */
28
+ export declare function channelCapabilityEnv(ctx: ConnectorContext): Promise<Record<string, string> | null>;
29
+ //# sourceMappingURL=channel-capability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"channel-capability.d.ts","sourceRoot":"","sources":["../src/channel-capability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AAkC1D,oFAAoF;AACpF,wBAAgB,2BAA2B,IAAI,IAAI,CAElD;AAcD;;;;GAIG;AACH,wBAAsB,oBAAoB,CACxC,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC,CA2CxC"}
@@ -0,0 +1,81 @@
1
+ import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
2
+ const MINT_TIMEOUT_MS = 5_000;
3
+ // Re-mint this long before the token expires so an env set injected at turn
4
+ // start stays valid through a long turn's tail.
5
+ const EXPIRY_SKEW_MS = 10 * 60 * 1000;
6
+ // After a 403 (no grant / revoked / flag off), skip minting for this long —
7
+ // revocation latency stays bounded while per-turn 403 noise does not.
8
+ const DENIED_TTL_MS = 60 * 1000;
9
+ // A GRANTED result is re-validated on the same cadence: the mint endpoint IS
10
+ // the revocation gate, so a positive cache lasting the token TTL (~2h) would
11
+ // extend revocation latency to hours. Re-minting is cheap (the platform
12
+ // Redis-caches the exchanged token and re-evaluates the gate per request);
13
+ // this cache only absorbs bursty emits within a conversation.
14
+ const GRANTED_TTL_MS = 60 * 1000;
15
+ // Per-connection cache. The plugin isolate is per-connection, so a plain
16
+ // module map keyed by connectionId is effectively single-entry — the key
17
+ // guards against isolate reuse across connections.
18
+ const cache = new Map();
19
+ /** Test hook: the cache is module-level state, so suites reset it between cases. */
20
+ export function resetChannelCapabilityCache() {
21
+ cache.clear();
22
+ }
23
+ function envFromMint(minted) {
24
+ return {
25
+ LARKSUITE_CLI_APP_ID: minted.app_id,
26
+ LARKSUITE_CLI_BRAND: minted.brand || 'feishu',
27
+ LARKSUITE_CLI_TENANT_ACCESS_TOKEN: minted.token,
28
+ // Bot lock: the credential is the app's, not a human's — mirror the
29
+ // bridge shim's injection exactly (agent-core channel-exec).
30
+ LARKSUITE_CLI_DEFAULT_AS: 'bot',
31
+ LARKSUITE_CLI_STRICT_MODE: 'bot',
32
+ };
33
+ }
34
+ /**
35
+ * Returns the feishu-cli env set to spread into an envelope's invocation
36
+ * context, or null when the capability is not granted (or the mint degraded
37
+ * — a transient platform error must never block a dispatch emit).
38
+ */
39
+ export async function channelCapabilityEnv(ctx) {
40
+ const key = ctx.connectionId;
41
+ const now = Date.now();
42
+ const hit = cache.get(key);
43
+ if (hit && hit.validUntil > now)
44
+ return hit.env;
45
+ try {
46
+ const res = await fetch(`${parallApiUrl(ctx)}/api/v1/orgs/${encodeURIComponent(parallOrgId(ctx))}/agents/me/channel-token`, {
47
+ method: 'POST',
48
+ headers: {
49
+ authorization: `Bearer ${requireAgk(ctx)}`,
50
+ 'content-type': 'application/json',
51
+ },
52
+ body: JSON.stringify({ channel_type: 'feishu' }),
53
+ signal: AbortSignal.timeout(MINT_TIMEOUT_MS),
54
+ });
55
+ if (res.status === 403 || res.status === 404) {
56
+ // No grant (never had it / revoked / flag off) — or an old server
57
+ // without the endpoint. Same agent-facing outcome: no capability.
58
+ cache.set(key, { env: null, validUntil: now + DENIED_TTL_MS });
59
+ return null;
60
+ }
61
+ if (!res.ok) {
62
+ // Transient (5xx/502-mint-upstream): don't cache, don't block the emit.
63
+ return null;
64
+ }
65
+ const minted = (await res.json());
66
+ if (!minted?.token || !minted.app_id)
67
+ return null;
68
+ const env = envFromMint(minted);
69
+ const expiresAt = Date.parse(minted.expires_at);
70
+ // Bounded by BOTH the revalidation cadence (revocation latency) and the
71
+ // token's own remaining life (an injected env must outlive the turn).
72
+ const tokenBound = Number.isFinite(expiresAt) ? expiresAt - EXPIRY_SKEW_MS : now;
73
+ const validUntil = Math.max(now + 5_000, Math.min(now + GRANTED_TTL_MS, tokenBound));
74
+ cache.set(key, { env, validUntil });
75
+ return env;
76
+ }
77
+ catch {
78
+ // Network failure — degrade to "no env this turn"; the next emit retries.
79
+ return null;
80
+ }
81
+ }
@@ -16,15 +16,15 @@
16
16
  *
17
17
  * - The audience is OUTSIDE Parall — no prll:// refs, no internal cards or
18
18
  * attachment links in the reply (dead links there).
19
- * - The reply goes out through the provider clip's `send_message` command.
20
- * A parel agent invokes clips from its sandbox via the parall CLI, so the
21
- * hint spells out the concrete `parall clip invoke …` command (agent-core
22
- * leaves the invocation mechanism to the bridge's own clip tooling).
23
- * - Provider metadata is best-effort (the connection lookup can fail) —
24
- * never instruct the agent to invoke a made-up clip alias.
19
+ * - Single-path reply (multi-channel-architecture-design §6): with the
20
+ * feishu-cli capability granted, the reply goes out through the official
21
+ * lark-cli auth rides the turn's invocation context env
22
+ * (channel-capability.ts), so bare `lark-cli` works in the sandbox.
23
+ * Without the grant there is no outbound path and the hint says so (the
24
+ * provider clip path is retired).
25
25
  * - When the inbound external message id is known, offer the in-thread reply
26
- * form ({"message_id": …}) so threaded conversations aren't nudged toward
27
- * new top-level messages.
26
+ * form so threaded conversations aren't nudged toward new top-level
27
+ * messages.
28
28
  */
29
29
  export interface PromptAttachment {
30
30
  id: string;
@@ -52,7 +52,7 @@ export interface ChatPromptArgs {
52
52
  */
53
53
  export declare function buildChatPrompt(args: ChatPromptArgs): string;
54
54
  export interface ChannelPromptArgs {
55
- /** Provider name (= clip alias, e.g. "feishu"); undefined when unresolved. */
55
+ /** Provider name (e.g. "feishu"); undefined when unresolved. */
56
56
  provider?: string;
57
57
  /** Conversation shape as recorded on the ChannelConversation (dm/group). */
58
58
  conversationType?: string;
@@ -61,6 +61,13 @@ export interface ChannelPromptArgs {
61
61
  senderName: string;
62
62
  /** The inbound message text (raw, un-framed). */
63
63
  text: string;
64
+ /**
65
+ * Live capability grant: the turn's invocation context carries the minted
66
+ * LARKSUITE_CLI_* env, so bare `lark-cli` works in the sandbox and is THE
67
+ * reply path. False → outbound is disabled for this org (no grant); the
68
+ * hint says so instead of pointing at the retired provider clip.
69
+ */
70
+ cliCapable?: boolean;
64
71
  }
65
72
  export declare function buildChannelPrompt(args: ChannelPromptArgs): string;
66
73
  //# sourceMappingURL=channel-prompt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"channel-prompt.d.ts","sourceRoot":"","sources":["../src/channel-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAClC;AAmBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAU5D;AAED,MAAM,WAAW,iBAAiB;IAChC,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAkBlE"}
1
+ {"version":3,"file":"channel-prompt.d.ts","sourceRoot":"","sources":["../src/channel-prompt.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,cAAc;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qEAAqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;CAClC;AAmBD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,MAAM,CAU5D;AAED,MAAM,WAAW,iBAAiB;IAChC,gEAAgE;IAChE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,4EAA4E;IAC5E,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,IAAI,EAAE,MAAM,CAAC;IACb;;;;;OAKG;IACH,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB;AAED,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,iBAAiB,GAAG,MAAM,CAkBlE"}
@@ -16,15 +16,15 @@
16
16
  *
17
17
  * - The audience is OUTSIDE Parall — no prll:// refs, no internal cards or
18
18
  * attachment links in the reply (dead links there).
19
- * - The reply goes out through the provider clip's `send_message` command.
20
- * A parel agent invokes clips from its sandbox via the parall CLI, so the
21
- * hint spells out the concrete `parall clip invoke …` command (agent-core
22
- * leaves the invocation mechanism to the bridge's own clip tooling).
23
- * - Provider metadata is best-effort (the connection lookup can fail) —
24
- * never instruct the agent to invoke a made-up clip alias.
19
+ * - Single-path reply (multi-channel-architecture-design §6): with the
20
+ * feishu-cli capability granted, the reply goes out through the official
21
+ * lark-cli auth rides the turn's invocation context env
22
+ * (channel-capability.ts), so bare `lark-cli` works in the sandbox.
23
+ * Without the grant there is no outbound path and the hint says so (the
24
+ * provider clip path is retired).
25
25
  * - When the inbound external message id is known, offer the in-thread reply
26
- * form ({"message_id": …}) so threaded conversations aren't nudged toward
27
- * new top-level messages.
26
+ * form so threaded conversations aren't nudged toward new top-level
27
+ * messages.
28
28
  */
29
29
  function sanitizeMeta(value) {
30
30
  return value
@@ -85,16 +85,22 @@ export function buildChannelPrompt(args) {
85
85
  return lines.join('\n') + buildChannelReplyHint(args);
86
86
  }
87
87
  function buildChannelReplyHint(args) {
88
- const target = JSON.stringify({
89
- chat_id: args.externalConversationId || '<conversation id>',
90
- text: '...',
91
- });
92
- const threadAlt = args.externalMessageId
93
- ? ` To reply in-thread to this specific message, use '${JSON.stringify({ message_id: args.externalMessageId, text: '...' })}' instead.`
94
- : '';
95
- if (args.provider) {
96
- const alias = args.provider;
97
- return `\n<system-reminder>To reply, invoke the \`${alias}\` clip's \`send_message\` command from your sandbox: \`parall clip invoke ${alias} send_message '${target}'\` — your plain text output is NOT delivered to the external conversation.${threadAlt} The same clip's \`call\` command reaches the wider ${alias} API when needed.</system-reminder>`;
88
+ // Single-path routing (multi-channel-architecture-design §6/§8.3): with
89
+ // the feishu-cli grant, the minted LARKSUITE_CLI_* env rides this turn's
90
+ // invocation context, so the official lark-cli works ambiently in the
91
+ // sandbox and is THE reply path. Without the grant there is no outbound
92
+ // path say so instead of pointing at the retired provider clip.
93
+ // cliCapable alone decides: the mint is feishu-specific, so a successful
94
+ // grant implies Feishu even when the cosmetic provider-label lookup failed.
95
+ if (args.cliCapable) {
96
+ const convRef = args.externalConversationId
97
+ ? `chat_id "${args.externalConversationId}"`
98
+ : 'the conversation id named in this event';
99
+ const threadAlt = args.externalMessageId
100
+ ? ` To reply threaded to this specific message, reference message_id "${args.externalMessageId}".`
101
+ : '';
102
+ return `\n<system-reminder>To reply, use the official Feishu CLI in your sandbox: send a message to ${convRef} with \`lark-cli im\` (see \`lark-cli im --help\` for send syntax; auth is provisioned in your environment — if lark-cli is missing, install it once with \`npm i -g @larksuite/cli\`).${threadAlt} lark-cli is the ONLY outbound path — your plain text output is NOT delivered to the external conversation.</system-reminder>`;
98
103
  }
99
- return `\n<system-reminder>To reply, invoke your channel provider clip's \`send_message\` command from your sandbox: \`parall clip invoke <alias> send_message '${target}'\` (find the alias with \`parall clip list\`) — your plain text output is NOT delivered to the external conversation.${threadAlt} The same clip's \`call\` command reaches the wider external platform API when needed.</system-reminder>`;
104
+ const platform = args.provider ?? 'the external platform';
105
+ return `\n<system-reminder>This message arrived from ${platform}, but outbound replies are currently disabled for this org (no channel capability granted). Do NOT attempt to reply on the external platform. If action is needed, surface it inside Parall (\`parall messages send\` / \`parall dm\`). Your plain text output is not delivered anywhere.</system-reminder>`;
100
106
  }
package/dist/fork.d.ts CHANGED
@@ -1,5 +1,8 @@
1
- import type { ConnectorContext } from '@parel/plugin-sdk';
2
- import type { AgentEventWithChild, ChildSpawnFailedEvent } from './parel-sdk-compat.js';
1
+ import type { AgentEvent, ConnectorContext } from '@parel/plugin-sdk';
2
+ /** The child-spawn failure variant of the sdk's AgentEvent union. */
3
+ type ChildSpawnFailedEvent = Extract<AgentEvent, {
4
+ type: 'child_spawn_failed';
5
+ }>;
3
6
  /** Delay before a failed spawn's messages are re-driven through onTimer. */
4
7
  export declare const FORK_RETRY_DELAY_MS = 15000;
5
8
  export declare const FORK_RETRY_PREFIX = "forkRetry:";
@@ -99,12 +102,12 @@ export declare function clearForkState(ctx: ConnectorContext): Promise<string[]>
99
102
  * Track main-session busyness from the E1 turn stream. Child events (they
100
103
  * carry childRef) must NOT feed this — only the main session's turns do.
101
104
  */
102
- export declare function noteMainTurnEvent(ctx: ConnectorContext, event: AgentEventWithChild): Promise<void>;
105
+ export declare function noteMainTurnEvent(ctx: ConnectorContext, event: AgentEvent): Promise<void>;
103
106
  /**
104
107
  * Resolve a child event's bookkeeping row (by its subject mirror) and fire
105
108
  * the pending spawn ack exactly once. Returns the ack request when due.
106
109
  */
107
- export declare function settleChildEvent(ctx: ConnectorContext, event: AgentEventWithChild): Promise<{
110
+ export declare function settleChildEvent(ctx: ConnectorContext, event: AgentEvent): Promise<{
108
111
  subject: string;
109
112
  pendingAcks: PendingAck[];
110
113
  } | null>;
@@ -145,4 +148,5 @@ export declare function handleChildSpawnFailed(ctx: ConnectorContext, event: Chi
145
148
  * covers them.
146
149
  */
147
150
  export declare function buildForkScopePrefix(subject: string, threadRootId?: string): string;
151
+ export {};
148
152
  //# sourceMappingURL=fork.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"fork.d.ts","sourceRoot":"","sources":["../src/fork.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"fork.d.ts","sourceRoot":"","sources":["../src/fork.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"inbound-channel.d.ts","sourceRoot":"","sources":["../src/inbound-channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAiB3E;;;;GAIG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,OAAQ,CAAC;AAEhD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CA6J5B"}
1
+ {"version":3,"file":"inbound-channel.d.ts","sourceRoot":"","sources":["../src/inbound-channel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAkB3E;;;;GAIG;AAEH;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,OAAQ,CAAC;AAEhD;;;;;;;;;;;;;GAaG;AACH,wBAAsB,yBAAyB,CAC7C,QAAQ,EAAE,MAAM,EAChB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAkK5B"}
@@ -1,3 +1,4 @@
1
+ import { channelCapabilityEnv } from './channel-capability.js';
1
2
  import { buildChannelPrompt } from './channel-prompt.js';
2
3
  import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
3
4
  import { ackEffect, createChannelInputStep, ensureSession, invalidateSession, markDispatchReceived, nextEnvelopeId, patchSession, readActiveEmit, REQUEST_TIMEOUT_MS, TURN_ENVELOPE_KEY, writeActiveEmit, } from './session.js';
@@ -116,6 +117,9 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
116
117
  }
117
118
  await receivedSettled;
118
119
  await writeActiveEmit(ctx, 'channel_message', sourceId, envelopeId);
120
+ // Live capability grant → this turn's lark-cli auth env (mint gate = the
121
+ // no-drift revocation point) + the hint teaching the single reply path.
122
+ const capabilityEnv = await channelCapabilityEnv(ctx);
119
123
  return [
120
124
  {
121
125
  type: 'emitEvent',
@@ -140,6 +144,7 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
140
144
  externalMessageId,
141
145
  senderName,
142
146
  text,
147
+ cliCapable: capabilityEnv !== null,
143
148
  }),
144
149
  conversationId,
145
150
  channelMessageId: sourceId,
@@ -147,19 +152,19 @@ export async function effectsForChannelDispatch(sourceId, ctx) {
147
152
  externalConversationId,
148
153
  externalMessageId,
149
154
  },
150
- // No chatId on purpose: the reply goes out through the provider clip
151
- // (agent-invoked), NOT deliver() — deliver only runs the redundant
152
- // session idle for channel routes (see delivery.ts).
155
+ // No chatId on purpose: the reply is the agent's own lark-cli send,
156
+ // NOT deliver() — deliver only runs the redundant session idle for
157
+ // channel routes (see delivery.ts).
153
158
  replyRoute: {
154
159
  kind: 'provider_http',
155
160
  connectionId: ctx.connectionId,
156
161
  data: { conversationId, envelopeId },
157
162
  },
158
- // No invocation context: PRLL_CHAT_ID must not point CLI defaults at
159
- // a chat that doesn't exist, and the reply targets (external ids)
160
- // already ride the prompt verbatim. (So no PRLL_DISPATCH_SOURCE_*
161
- // either the external reply goes out through the provider clip,
162
- // and this row's ledger close is the turn event's complete-sources.)
163
+ // Invocation context carries ONLY the capability env: no PRLL_CHAT_ID
164
+ // (it must not point CLI defaults at a chat that doesn't exist) and
165
+ // no PRLL_DISPATCH_SOURCE_* (the external reply is a vendor-CLI send,
166
+ // and this row's ledger close is the turn event's complete-sources).
167
+ ...(capabilityEnv ? { context: capabilityEnv } : {}),
163
168
  },
164
169
  },
165
170
  // No ack effect — see the message path (inbound.ts): the row stays
@@ -1 +1 @@
1
- {"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../src/inbound.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACf,MAAM,mBAAmB,CAAC;AAuH3B;kDACkD;AAClD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CASjE;AACD,wBAAgB,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAO9D;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAG5B;AAwCD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EACtB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAE5B;AA8BD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAE/E;AAwpBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAE7B"}
1
+ {"version":3,"file":"inbound.d.ts","sourceRoot":"","sources":["../src/inbound.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,UAAU,EACV,gBAAgB,EAChB,gBAAgB,EAChB,eAAe,EACf,cAAc,EACf,MAAM,mBAAmB,CAAC;AAkH3B;kDACkD;AAClD,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,gBAAgB,GAAG,IAAI,CASjE;AACD,wBAAgB,UAAU,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAO9D;AAED,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,cAAc,EACrB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAG5B;AAwCD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE;IAAE,GAAG,EAAE,MAAM,CAAA;CAAE,EACtB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,eAAe,EAAE,CAAC,CAE5B;AA8BD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC,CAE/E;AAsqBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,UAAU,EACjB,GAAG,EAAE,gBAAgB,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC,CAE7B"}
package/dist/inbound.js CHANGED
@@ -1,8 +1,8 @@
1
+ import { channelCapabilityEnv } from './channel-capability.js';
1
2
  import { buildChatPrompt } from './channel-prompt.js';
2
3
  import { parallApiUrl, parallOrgId, requireAgk, sandboxEnabled } from './connect.js';
3
4
  import { effectsForChannelDispatch, PROVIDER_LOOKUP_TIMEOUT_MS } from './inbound-channel.js';
4
5
  import { buildForkScopePrefix, bumpForkRetryAttempts, childConfirmed, childRefKnown, clearForkState, clearForkSubject, FORK_RETRY_DELAY_MS, FORK_RETRY_PREFIX, handleChildSpawnFailed, MAX_SPAWN_VERIFY_ATTEMPTS, noteMainTurnEvent, planForkDecision, recordSpawn, settleChildEvent, trackFollowUpAck, } from './fork.js';
5
- import { asChildSpawnFailed, asConnectorEffects, } from './parel-sdk-compat.js';
6
6
  import { ackByIdEffect, ackEffect, completeFencedEnvelopes, conversationKey, createErrorStep, fetchMainSessionFresh, createInputStep, createTraceStep, createTypedInputStep, EMIT_TTL_MS, ensureChildSession, ensureSession, invalidateChildSession, invalidateSession, markDispatchReceived, nextEnvelopeId, patchSession, readActiveEmit, reportingEnabled, SESSION_CACHE_KEY, TURN_ENVELOPE_KEY, writeActiveEmit, } from './session.js';
7
7
  import { isTypedEventType, planTypedDispatch } from './typed-dispatch.js';
8
8
  /**
@@ -356,12 +356,18 @@ async function effectsForDispatch(data, ctx) {
356
356
  // so the server resolves this WorkItem in the reply's own transaction.
357
357
  // (Fork turns get it too — their rows are legacy-acked so the resolve is a
358
358
  // no-op today, and it wires up for free when fork ledger parity lands.)
359
+ // Channel-capability env (lark-cli auth) rides EVERY turn's invocation
360
+ // context while granted — Feishu workspace ops (docs/calendar/…) are asked
361
+ // for from ordinary Parall chats, not only from channel turns. Null when
362
+ // ungranted or the mint degrades; never blocks the emit.
363
+ const capabilityEnv = await channelCapabilityEnv(ctx);
359
364
  const invocationContext = {
360
365
  PRLL_CHAT_ID: chatId,
361
366
  PRLL_TRIGGER_MESSAGE_ID: sourceId,
362
367
  PRLL_DISPATCH_SOURCE_TYPE: sourceType,
363
368
  PRLL_DISPATCH_SOURCE_ID: sourceId,
364
369
  ...(threadRootId ? { PRLL_THREAD_ROOT_ID: threadRootId } : {}),
370
+ ...(capabilityEnv ?? {}),
365
371
  };
366
372
  const envelope = {
367
373
  id: envelopeId,
@@ -445,18 +451,16 @@ async function effectsForDispatch(data, ctx) {
445
451
  source_id: sourceId,
446
452
  chat_id: chatId,
447
453
  });
448
- return asConnectorEffects([
449
- { type: 'setTimer', key: retryKey, at: ctx.now() + FORK_RETRY_DELAY_MS },
450
- ]);
454
+ return [{ type: 'setTimer', key: retryKey, at: ctx.now() + FORK_RETRY_DELAY_MS }];
451
455
  }
452
- return asConnectorEffects([
456
+ return [
453
457
  { type: 'emitEvent', event: childEnvelope, deliverTo: { childRef: decision.childRef } },
454
- ]);
458
+ ];
455
459
  }
456
- return asConnectorEffects([
460
+ return [
457
461
  { type: 'emitEvent', event: childEnvelope, deliverTo: { childRef: decision.childRef } },
458
462
  ackEffect(ctx, sourceType, sourceId),
459
- ]);
463
+ ];
460
464
  }
461
465
  if (decision.mode === 'spawn') {
462
466
  // Main is mid-turn on another conversation — fork. The opening message
@@ -519,7 +523,14 @@ async function effectsForDispatch(data, ctx) {
519
523
  }
520
524
  await recordSpawn(ctx, subject, decision.childRef, { dispatchId, sourceId });
521
525
  await receivedSettled;
522
- return asConnectorEffects([
526
+ // Known gap: SpawnChildSessionEffect (#140) carries no invocation
527
+ // context, so the fork's OPENING turn runs without the capability env
528
+ // (lark-cli workspace ops would need a follow-up turn — those arrive
529
+ // as deliverTo envelopes, which inherit the full context). Channel
530
+ // replies are unaffected: channel_message dispatches always ride main.
531
+ // Fix belongs parel-side (context on spawnChildSession) — tracked as a
532
+ // parel ask.
533
+ return [
523
534
  {
524
535
  type: 'spawnChildSession',
525
536
  childRef: decision.childRef,
@@ -527,7 +538,7 @@ async function effectsForDispatch(data, ctx) {
527
538
  subject,
528
539
  },
529
540
  ...timerEffects,
530
- ]);
541
+ ];
531
542
  }
532
543
  // fall through: black-holed spawn rides main below.
533
544
  }
@@ -647,6 +658,15 @@ async function effectsForTypedDispatch(data, ctx) {
647
658
  }
648
659
  }
649
660
  }
661
+ // Typed turns carry the capability env too: a task or schedule is exactly
662
+ // where "create the Feishu doc / check the calendar" work is assigned, so
663
+ // the every-turn capability contract covers them (null when ungranted or
664
+ // the mint degrades; never blocks the emit).
665
+ const capabilityEnv = await channelCapabilityEnv(ctx);
666
+ const typedContext = {
667
+ ...(plan.chatId ? { PRLL_CHAT_ID: plan.chatId, PRLL_TRIGGER_MESSAGE_ID: sourceId } : {}),
668
+ ...(capabilityEnv ?? {}),
669
+ };
650
670
  return [
651
671
  {
652
672
  type: 'emitEvent',
@@ -673,14 +693,7 @@ async function effectsForTypedDispatch(data, ctx) {
673
693
  ? { chatId: plan.chatId, messageId: sourceId, envelopeId }
674
694
  : { envelopeId },
675
695
  },
676
- ...(plan.chatId
677
- ? {
678
- context: {
679
- PRLL_CHAT_ID: plan.chatId,
680
- PRLL_TRIGGER_MESSAGE_ID: sourceId,
681
- },
682
- }
683
- : {}),
696
+ ...(Object.keys(typedContext).length > 0 ? { context: typedContext } : {}),
684
697
  },
685
698
  },
686
699
  ack(),
@@ -765,22 +778,21 @@ export function handleParallAgentEvent(event, ctx) {
765
778
  return serialized(() => handleParallAgentEventInner(event, ctx));
766
779
  }
767
780
  async function handleParallAgentEventInner(event, ctx) {
768
- // spawnChildSession's asynchronous error channel — not turn-scoped, pushed
769
- // regardless of observe scopes, and shaped outside the published AgentEvent
770
- // union (see parel-sdk-compat). Handle it before anything subject-gated.
771
- const spawnFailed = asChildSpawnFailed(event);
772
- if (spawnFailed) {
781
+ // spawnChildSession's asynchronous error channel — not turn-scoped and
782
+ // pushed regardless of observe scopes. Handle it before anything
783
+ // subject-gated.
784
+ if (event.type === 'child_spawn_failed') {
773
785
  const effects = [];
774
786
  try {
775
- const outcome = await handleChildSpawnFailed(ctx, spawnFailed);
787
+ const outcome = await handleChildSpawnFailed(ctx, event);
776
788
  if (outcome.cleared) {
777
789
  // The spawn's ase_ was already created active with an input step; no
778
790
  // child turn will ever emit a terminal event for it — settle it here
779
791
  // or the session (and its processing indicator) stays active forever.
780
- const orphanAse = await ensureChildSession(ctx, spawnFailed.childRef);
792
+ const orphanAse = await ensureChildSession(ctx, event.childRef);
781
793
  if (orphanAse)
782
794
  await patchSession(ctx, orphanAse, { status: 'idle' });
783
- await invalidateChildSession(ctx, spawnFailed.childRef);
795
+ await invalidateChildSession(ctx, event.childRef);
784
796
  }
785
797
  // Re-drive the stranded messages WITHOUT waiting for a reconnect sweep
786
798
  // (a healthy long-lived socket never re-runs onOpen): park their
@@ -806,15 +818,14 @@ async function handleParallAgentEventInner(event, ctx) {
806
818
  }
807
819
  return effects;
808
820
  }
809
- const withChild = event;
810
- if (withChild.childRef) {
811
- return handleChildAgentEvent(withChild, ctx);
821
+ if (event.childRef) {
822
+ return handleChildAgentEvent(event, ctx);
812
823
  }
813
824
  const chatId = event.subject;
814
825
  try {
815
826
  // Best-effort busy classifier for fork-on-busy: only MAIN turns feed it
816
827
  // (child events took the branch above).
817
- await noteMainTurnEvent(ctx, withChild).catch(() => { });
828
+ await noteMainTurnEvent(ctx, event).catch(() => { });
818
829
  if (!chatId)
819
830
  return [];
820
831
  if (event.type === 'turn_completed' || event.type === 'turn_failed') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/parel-channel",
3
- "version": "1.42.0",
3
+ "version": "1.43.0",
4
4
  "description": "Parall channel plugin for the parel runtime — lets a parel agent treat Parall as a managed_ws channel (receive dispatches, reply via the Parall API)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -23,7 +23,7 @@
23
23
  "parel.plugin.json"
24
24
  ],
25
25
  "dependencies": {
26
- "@parel/plugin-sdk": "^0.9.0"
26
+ "@parel/plugin-sdk": "^0.12.0"
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^22.0.0",
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Channel-capability credential injection — the parel-native form of the
3
+ * feishu-cli broker (multi-channel-architecture-design §8.3).
4
+ *
5
+ * Bridges deliver the capability as a PATH pointer shim that mints a fresh
6
+ * short-lived tenant token per lark-cli invocation. In parel we control
7
+ * neither the sandbox PATH nor a bridge process — but we DO control the
8
+ * per-turn invocation context, which sandbox-e2b flattens into every exec's
9
+ * env. So the connector mints at emit time and injects the official
10
+ * LARKSUITE_CLI_* env set: bare `lark-cli` works ambiently for the whole
11
+ * turn (turn length ≪ token TTL ~2h), the app secret never leaves the
12
+ * platform, and revocation bites on the next emit's mint (403 → no env).
13
+ *
14
+ * The mint endpoint IS the gate (the platform re-evaluates connection state
15
+ * + flag on every request — the no-drift invariant), so the connector needs
16
+ * no declaration-plane fetch: a 403 is the revocation signal and is
17
+ * negative-cached briefly to keep a revoked grant from adding one 403 per
18
+ * turn.
19
+ */
20
+ import type { ConnectorContext } from '@parel/plugin-sdk';
21
+ import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
22
+
23
+ const MINT_TIMEOUT_MS = 5_000;
24
+ // Re-mint this long before the token expires so an env set injected at turn
25
+ // start stays valid through a long turn's tail.
26
+ const EXPIRY_SKEW_MS = 10 * 60 * 1000;
27
+ // After a 403 (no grant / revoked / flag off), skip minting for this long —
28
+ // revocation latency stays bounded while per-turn 403 noise does not.
29
+ const DENIED_TTL_MS = 60 * 1000;
30
+ // A GRANTED result is re-validated on the same cadence: the mint endpoint IS
31
+ // the revocation gate, so a positive cache lasting the token TTL (~2h) would
32
+ // extend revocation latency to hours. Re-minting is cheap (the platform
33
+ // Redis-caches the exchanged token and re-evaluates the gate per request);
34
+ // this cache only absorbs bursty emits within a conversation.
35
+ const GRANTED_TTL_MS = 60 * 1000;
36
+
37
+ interface MintedToken {
38
+ app_id: string;
39
+ brand: string;
40
+ token: string;
41
+ expires_at: string;
42
+ }
43
+
44
+ interface CacheEntry {
45
+ env: Record<string, string> | null; // null = denied (negative cache)
46
+ validUntil: number;
47
+ }
48
+
49
+ // Per-connection cache. The plugin isolate is per-connection, so a plain
50
+ // module map keyed by connectionId is effectively single-entry — the key
51
+ // guards against isolate reuse across connections.
52
+ const cache = new Map<string, CacheEntry>();
53
+
54
+ /** Test hook: the cache is module-level state, so suites reset it between cases. */
55
+ export function resetChannelCapabilityCache(): void {
56
+ cache.clear();
57
+ }
58
+
59
+ function envFromMint(minted: MintedToken): Record<string, string> {
60
+ return {
61
+ LARKSUITE_CLI_APP_ID: minted.app_id,
62
+ LARKSUITE_CLI_BRAND: minted.brand || 'feishu',
63
+ LARKSUITE_CLI_TENANT_ACCESS_TOKEN: minted.token,
64
+ // Bot lock: the credential is the app's, not a human's — mirror the
65
+ // bridge shim's injection exactly (agent-core channel-exec).
66
+ LARKSUITE_CLI_DEFAULT_AS: 'bot',
67
+ LARKSUITE_CLI_STRICT_MODE: 'bot',
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Returns the feishu-cli env set to spread into an envelope's invocation
73
+ * context, or null when the capability is not granted (or the mint degraded
74
+ * — a transient platform error must never block a dispatch emit).
75
+ */
76
+ export async function channelCapabilityEnv(
77
+ ctx: ConnectorContext,
78
+ ): Promise<Record<string, string> | null> {
79
+ const key = ctx.connectionId;
80
+ const now = Date.now();
81
+ const hit = cache.get(key);
82
+ if (hit && hit.validUntil > now) return hit.env;
83
+
84
+ try {
85
+ const res = await fetch(
86
+ `${parallApiUrl(ctx)}/api/v1/orgs/${encodeURIComponent(parallOrgId(ctx))}/agents/me/channel-token`,
87
+ {
88
+ method: 'POST',
89
+ headers: {
90
+ authorization: `Bearer ${requireAgk(ctx)}`,
91
+ 'content-type': 'application/json',
92
+ },
93
+ body: JSON.stringify({ channel_type: 'feishu' }),
94
+ signal: AbortSignal.timeout(MINT_TIMEOUT_MS),
95
+ },
96
+ );
97
+ if (res.status === 403 || res.status === 404) {
98
+ // No grant (never had it / revoked / flag off) — or an old server
99
+ // without the endpoint. Same agent-facing outcome: no capability.
100
+ cache.set(key, { env: null, validUntil: now + DENIED_TTL_MS });
101
+ return null;
102
+ }
103
+ if (!res.ok) {
104
+ // Transient (5xx/502-mint-upstream): don't cache, don't block the emit.
105
+ return null;
106
+ }
107
+ const minted = (await res.json()) as MintedToken;
108
+ if (!minted?.token || !minted.app_id) return null;
109
+ const env = envFromMint(minted);
110
+ const expiresAt = Date.parse(minted.expires_at);
111
+ // Bounded by BOTH the revalidation cadence (revocation latency) and the
112
+ // token's own remaining life (an injected env must outlive the turn).
113
+ const tokenBound = Number.isFinite(expiresAt) ? expiresAt - EXPIRY_SKEW_MS : now;
114
+ const validUntil = Math.max(now + 5_000, Math.min(now + GRANTED_TTL_MS, tokenBound));
115
+ cache.set(key, { env, validUntil });
116
+ return env;
117
+ } catch {
118
+ // Network failure — degrade to "no env this turn"; the next emit retries.
119
+ return null;
120
+ }
121
+ }
@@ -16,15 +16,15 @@
16
16
  *
17
17
  * - The audience is OUTSIDE Parall — no prll:// refs, no internal cards or
18
18
  * attachment links in the reply (dead links there).
19
- * - The reply goes out through the provider clip's `send_message` command.
20
- * A parel agent invokes clips from its sandbox via the parall CLI, so the
21
- * hint spells out the concrete `parall clip invoke …` command (agent-core
22
- * leaves the invocation mechanism to the bridge's own clip tooling).
23
- * - Provider metadata is best-effort (the connection lookup can fail) —
24
- * never instruct the agent to invoke a made-up clip alias.
19
+ * - Single-path reply (multi-channel-architecture-design §6): with the
20
+ * feishu-cli capability granted, the reply goes out through the official
21
+ * lark-cli auth rides the turn's invocation context env
22
+ * (channel-capability.ts), so bare `lark-cli` works in the sandbox.
23
+ * Without the grant there is no outbound path and the hint says so (the
24
+ * provider clip path is retired).
25
25
  * - When the inbound external message id is known, offer the in-thread reply
26
- * form ({"message_id": …}) so threaded conversations aren't nudged toward
27
- * new top-level messages.
26
+ * form so threaded conversations aren't nudged toward new top-level
27
+ * messages.
28
28
  */
29
29
 
30
30
  function sanitizeMeta(value: string): string {
@@ -90,7 +90,7 @@ export function buildChatPrompt(args: ChatPromptArgs): string {
90
90
  }
91
91
 
92
92
  export interface ChannelPromptArgs {
93
- /** Provider name (= clip alias, e.g. "feishu"); undefined when unresolved. */
93
+ /** Provider name (e.g. "feishu"); undefined when unresolved. */
94
94
  provider?: string;
95
95
  /** Conversation shape as recorded on the ChannelConversation (dm/group). */
96
96
  conversationType?: string;
@@ -99,6 +99,13 @@ export interface ChannelPromptArgs {
99
99
  senderName: string;
100
100
  /** The inbound message text (raw, un-framed). */
101
101
  text: string;
102
+ /**
103
+ * Live capability grant: the turn's invocation context carries the minted
104
+ * LARKSUITE_CLI_* env, so bare `lark-cli` works in the sandbox and is THE
105
+ * reply path. False → outbound is disabled for this org (no grant); the
106
+ * hint says so instead of pointing at the retired provider clip.
107
+ */
108
+ cliCapable?: boolean;
102
109
  }
103
110
 
104
111
  export function buildChannelPrompt(args: ChannelPromptArgs): string {
@@ -122,16 +129,22 @@ export function buildChannelPrompt(args: ChannelPromptArgs): string {
122
129
  }
123
130
 
124
131
  function buildChannelReplyHint(args: ChannelPromptArgs): string {
125
- const target = JSON.stringify({
126
- chat_id: args.externalConversationId || '<conversation id>',
127
- text: '...',
128
- });
129
- const threadAlt = args.externalMessageId
130
- ? ` To reply in-thread to this specific message, use '${JSON.stringify({ message_id: args.externalMessageId, text: '...' })}' instead.`
131
- : '';
132
- if (args.provider) {
133
- const alias = args.provider;
134
- return `\n<system-reminder>To reply, invoke the \`${alias}\` clip's \`send_message\` command from your sandbox: \`parall clip invoke ${alias} send_message '${target}'\` — your plain text output is NOT delivered to the external conversation.${threadAlt} The same clip's \`call\` command reaches the wider ${alias} API when needed.</system-reminder>`;
132
+ // Single-path routing (multi-channel-architecture-design §6/§8.3): with
133
+ // the feishu-cli grant, the minted LARKSUITE_CLI_* env rides this turn's
134
+ // invocation context, so the official lark-cli works ambiently in the
135
+ // sandbox and is THE reply path. Without the grant there is no outbound
136
+ // path say so instead of pointing at the retired provider clip.
137
+ // cliCapable alone decides: the mint is feishu-specific, so a successful
138
+ // grant implies Feishu even when the cosmetic provider-label lookup failed.
139
+ if (args.cliCapable) {
140
+ const convRef = args.externalConversationId
141
+ ? `chat_id "${args.externalConversationId}"`
142
+ : 'the conversation id named in this event';
143
+ const threadAlt = args.externalMessageId
144
+ ? ` To reply threaded to this specific message, reference message_id "${args.externalMessageId}".`
145
+ : '';
146
+ return `\n<system-reminder>To reply, use the official Feishu CLI in your sandbox: send a message to ${convRef} with \`lark-cli im\` (see \`lark-cli im --help\` for send syntax; auth is provisioned in your environment — if lark-cli is missing, install it once with \`npm i -g @larksuite/cli\`).${threadAlt} lark-cli is the ONLY outbound path — your plain text output is NOT delivered to the external conversation.</system-reminder>`;
135
147
  }
136
- return `\n<system-reminder>To reply, invoke your channel provider clip's \`send_message\` command from your sandbox: \`parall clip invoke <alias> send_message '${target}'\` (find the alias with \`parall clip list\`) — your plain text output is NOT delivered to the external conversation.${threadAlt} The same clip's \`call\` command reaches the wider external platform API when needed.</system-reminder>`;
148
+ const platform = args.provider ?? 'the external platform';
149
+ return `\n<system-reminder>This message arrived from ${platform}, but outbound replies are currently disabled for this org (no channel capability granted). Do NOT attempt to reply on the external platform. If action is needed, surface it inside Parall (\`parall messages send\` / \`parall dm\`). Your plain text output is not delivered anywhere.</system-reminder>`;
137
150
  }
package/src/fork.ts CHANGED
@@ -1,5 +1,7 @@
1
- import type { ConnectorContext } from '@parel/plugin-sdk';
2
- import type { AgentEventWithChild, ChildSpawnFailedEvent } from './parel-sdk-compat.js';
1
+ import type { AgentEvent, ConnectorContext } from '@parel/plugin-sdk';
2
+
3
+ /** The child-spawn failure variant of the sdk's AgentEvent union. */
4
+ type ChildSpawnFailedEvent = Extract<AgentEvent, { type: 'child_spawn_failed' }>;
3
5
 
4
6
  /**
5
7
  * Fork-on-busy scheduling (the parall-side half of parel F4, parel-mono #140;
@@ -298,10 +300,7 @@ export async function clearForkState(ctx: ConnectorContext): Promise<string[]> {
298
300
  * Track main-session busyness from the E1 turn stream. Child events (they
299
301
  * carry childRef) must NOT feed this — only the main session's turns do.
300
302
  */
301
- export async function noteMainTurnEvent(
302
- ctx: ConnectorContext,
303
- event: AgentEventWithChild,
304
- ): Promise<void> {
303
+ export async function noteMainTurnEvent(ctx: ConnectorContext, event: AgentEvent): Promise<void> {
305
304
  if (event.type === 'turn_started') {
306
305
  const row: MainTurnRow = { turnId: event.turnId, subject: event.subject, at: ctx.now() };
307
306
  await ctx.store?.set(MAIN_TURN_KEY, row).catch(() => {});
@@ -323,7 +322,7 @@ export async function noteMainTurnEvent(
323
322
  */
324
323
  export async function settleChildEvent(
325
324
  ctx: ConnectorContext,
326
- event: AgentEventWithChild,
325
+ event: AgentEvent,
327
326
  ): Promise<{ subject: string; pendingAcks: PendingAck[] } | null> {
328
327
  const childRef = event.childRef;
329
328
  if (!childRef) return null;
@@ -1,4 +1,5 @@
1
1
  import type { ConnectorContext, ConnectorEffect } from '@parel/plugin-sdk';
2
+ import { channelCapabilityEnv } from './channel-capability.js';
2
3
  import { buildChannelPrompt } from './channel-prompt.js';
3
4
  import { parallApiUrl, parallOrgId, requireAgk } from './connect.js';
4
5
  import {
@@ -154,6 +155,10 @@ export async function effectsForChannelDispatch(
154
155
  await receivedSettled;
155
156
  await writeActiveEmit(ctx, 'channel_message', sourceId, envelopeId);
156
157
 
158
+ // Live capability grant → this turn's lark-cli auth env (mint gate = the
159
+ // no-drift revocation point) + the hint teaching the single reply path.
160
+ const capabilityEnv = await channelCapabilityEnv(ctx);
161
+
157
162
  return [
158
163
  {
159
164
  type: 'emitEvent',
@@ -178,6 +183,7 @@ export async function effectsForChannelDispatch(
178
183
  externalMessageId,
179
184
  senderName,
180
185
  text,
186
+ cliCapable: capabilityEnv !== null,
181
187
  }),
182
188
  conversationId,
183
189
  channelMessageId: sourceId,
@@ -185,19 +191,19 @@ export async function effectsForChannelDispatch(
185
191
  externalConversationId,
186
192
  externalMessageId,
187
193
  },
188
- // No chatId on purpose: the reply goes out through the provider clip
189
- // (agent-invoked), NOT deliver() — deliver only runs the redundant
190
- // session idle for channel routes (see delivery.ts).
194
+ // No chatId on purpose: the reply is the agent's own lark-cli send,
195
+ // NOT deliver() — deliver only runs the redundant session idle for
196
+ // channel routes (see delivery.ts).
191
197
  replyRoute: {
192
198
  kind: 'provider_http',
193
199
  connectionId: ctx.connectionId,
194
200
  data: { conversationId, envelopeId },
195
201
  },
196
- // No invocation context: PRLL_CHAT_ID must not point CLI defaults at
197
- // a chat that doesn't exist, and the reply targets (external ids)
198
- // already ride the prompt verbatim. (So no PRLL_DISPATCH_SOURCE_*
199
- // either the external reply goes out through the provider clip,
200
- // and this row's ledger close is the turn event's complete-sources.)
202
+ // Invocation context carries ONLY the capability env: no PRLL_CHAT_ID
203
+ // (it must not point CLI defaults at a chat that doesn't exist) and
204
+ // no PRLL_DISPATCH_SOURCE_* (the external reply is a vendor-CLI send,
205
+ // and this row's ledger close is the turn event's complete-sources).
206
+ ...(capabilityEnv ? { context: capabilityEnv } : {}),
201
207
  },
202
208
  },
203
209
  // No ack effect — see the message path (inbound.ts): the row stays
package/src/inbound.ts CHANGED
@@ -5,6 +5,7 @@ import type {
5
5
  ConnectorEffect,
6
6
  WebSocketFrame,
7
7
  } from '@parel/plugin-sdk';
8
+ import { channelCapabilityEnv } from './channel-capability.js';
8
9
  import { buildChatPrompt, type PromptAttachment } from './channel-prompt.js';
9
10
  import { parallApiUrl, parallOrgId, requireAgk, sandboxEnabled } from './connect.js';
10
11
  import { effectsForChannelDispatch, PROVIDER_LOOKUP_TIMEOUT_MS } from './inbound-channel.js';
@@ -25,12 +26,6 @@ import {
25
26
  settleChildEvent,
26
27
  trackFollowUpAck,
27
28
  } from './fork.js';
28
- import {
29
- type AgentEventWithChild,
30
- asChildSpawnFailed,
31
- asConnectorEffects,
32
- type ExtendedConnectorEffect,
33
- } from './parel-sdk-compat.js';
34
29
  import {
35
30
  ackByIdEffect,
36
31
  ackEffect,
@@ -496,12 +491,18 @@ async function effectsForDispatch(
496
491
  // so the server resolves this WorkItem in the reply's own transaction.
497
492
  // (Fork turns get it too — their rows are legacy-acked so the resolve is a
498
493
  // no-op today, and it wires up for free when fork ledger parity lands.)
494
+ // Channel-capability env (lark-cli auth) rides EVERY turn's invocation
495
+ // context while granted — Feishu workspace ops (docs/calendar/…) are asked
496
+ // for from ordinary Parall chats, not only from channel turns. Null when
497
+ // ungranted or the mint degrades; never blocks the emit.
498
+ const capabilityEnv = await channelCapabilityEnv(ctx);
499
499
  const invocationContext: Record<string, string> = {
500
500
  PRLL_CHAT_ID: chatId,
501
501
  PRLL_TRIGGER_MESSAGE_ID: sourceId,
502
502
  PRLL_DISPATCH_SOURCE_TYPE: sourceType,
503
503
  PRLL_DISPATCH_SOURCE_ID: sourceId,
504
504
  ...(threadRootId ? { PRLL_THREAD_ROOT_ID: threadRootId } : {}),
505
+ ...(capabilityEnv ?? {}),
505
506
  };
506
507
 
507
508
  const envelope = {
@@ -586,18 +587,16 @@ async function effectsForDispatch(
586
587
  source_id: sourceId,
587
588
  chat_id: chatId,
588
589
  });
589
- return asConnectorEffects([
590
- { type: 'setTimer', key: retryKey, at: ctx.now() + FORK_RETRY_DELAY_MS },
591
- ]);
590
+ return [{ type: 'setTimer', key: retryKey, at: ctx.now() + FORK_RETRY_DELAY_MS }];
592
591
  }
593
- return asConnectorEffects([
592
+ return [
594
593
  { type: 'emitEvent', event: childEnvelope, deliverTo: { childRef: decision.childRef } },
595
- ]);
594
+ ];
596
595
  }
597
- return asConnectorEffects([
596
+ return [
598
597
  { type: 'emitEvent', event: childEnvelope, deliverTo: { childRef: decision.childRef } },
599
598
  ackEffect(ctx, sourceType, sourceId),
600
- ]);
599
+ ];
601
600
  }
602
601
 
603
602
  if (decision.mode === 'spawn') {
@@ -625,7 +624,7 @@ async function effectsForDispatch(
625
624
  // would leave the message stranded with no timer and no bookkeeping
626
625
  // until a reconnect).
627
626
  let spawnBlackHoled = false;
628
- const timerEffects: ExtendedConnectorEffect[] = [];
627
+ const timerEffects: ConnectorEffect[] = [];
629
628
  if (dispatchId) {
630
629
  const retryKey = `${FORK_RETRY_PREFIX}${dispatchId}`;
631
630
  const attempts = await bumpForkRetryAttempts(ctx, retryKey, {
@@ -659,7 +658,14 @@ async function effectsForDispatch(
659
658
  }
660
659
  await recordSpawn(ctx, subject, decision.childRef, { dispatchId, sourceId });
661
660
  await receivedSettled;
662
- return asConnectorEffects([
661
+ // Known gap: SpawnChildSessionEffect (#140) carries no invocation
662
+ // context, so the fork's OPENING turn runs without the capability env
663
+ // (lark-cli workspace ops would need a follow-up turn — those arrive
664
+ // as deliverTo envelopes, which inherit the full context). Channel
665
+ // replies are unaffected: channel_message dispatches always ride main.
666
+ // Fix belongs parel-side (context on spawnChildSession) — tracked as a
667
+ // parel ask.
668
+ return [
663
669
  {
664
670
  type: 'spawnChildSession',
665
671
  childRef: decision.childRef,
@@ -667,7 +673,7 @@ async function effectsForDispatch(
667
673
  subject,
668
674
  },
669
675
  ...timerEffects,
670
- ]);
676
+ ];
671
677
  }
672
678
  // fall through: black-holed spawn rides main below.
673
679
  }
@@ -796,6 +802,16 @@ async function effectsForTypedDispatch(
796
802
  }
797
803
  }
798
804
 
805
+ // Typed turns carry the capability env too: a task or schedule is exactly
806
+ // where "create the Feishu doc / check the calendar" work is assigned, so
807
+ // the every-turn capability contract covers them (null when ungranted or
808
+ // the mint degrades; never blocks the emit).
809
+ const capabilityEnv = await channelCapabilityEnv(ctx);
810
+ const typedContext: Record<string, string> = {
811
+ ...(plan.chatId ? { PRLL_CHAT_ID: plan.chatId, PRLL_TRIGGER_MESSAGE_ID: sourceId } : {}),
812
+ ...(capabilityEnv ?? {}),
813
+ };
814
+
799
815
  return [
800
816
  {
801
817
  type: 'emitEvent',
@@ -822,14 +838,7 @@ async function effectsForTypedDispatch(
822
838
  ? { chatId: plan.chatId, messageId: sourceId, envelopeId }
823
839
  : { envelopeId },
824
840
  },
825
- ...(plan.chatId
826
- ? {
827
- context: {
828
- PRLL_CHAT_ID: plan.chatId,
829
- PRLL_TRIGGER_MESSAGE_ID: sourceId,
830
- },
831
- }
832
- : {}),
841
+ ...(Object.keys(typedContext).length > 0 ? { context: typedContext } : {}),
833
842
  },
834
843
  },
835
844
  ack(),
@@ -932,21 +941,20 @@ async function handleParallAgentEventInner(
932
941
  event: AgentEvent,
933
942
  ctx: ConnectorContext,
934
943
  ): Promise<AgentEventEffect[]> {
935
- // spawnChildSession's asynchronous error channel — not turn-scoped, pushed
936
- // regardless of observe scopes, and shaped outside the published AgentEvent
937
- // union (see parel-sdk-compat). Handle it before anything subject-gated.
938
- const spawnFailed = asChildSpawnFailed(event);
939
- if (spawnFailed) {
944
+ // spawnChildSession's asynchronous error channel — not turn-scoped and
945
+ // pushed regardless of observe scopes. Handle it before anything
946
+ // subject-gated.
947
+ if (event.type === 'child_spawn_failed') {
940
948
  const effects: AgentEventEffect[] = [];
941
949
  try {
942
- const outcome = await handleChildSpawnFailed(ctx, spawnFailed);
950
+ const outcome = await handleChildSpawnFailed(ctx, event);
943
951
  if (outcome.cleared) {
944
952
  // The spawn's ase_ was already created active with an input step; no
945
953
  // child turn will ever emit a terminal event for it — settle it here
946
954
  // or the session (and its processing indicator) stays active forever.
947
- const orphanAse = await ensureChildSession(ctx, spawnFailed.childRef);
955
+ const orphanAse = await ensureChildSession(ctx, event.childRef);
948
956
  if (orphanAse) await patchSession(ctx, orphanAse, { status: 'idle' });
949
- await invalidateChildSession(ctx, spawnFailed.childRef);
957
+ await invalidateChildSession(ctx, event.childRef);
950
958
  }
951
959
  // Re-drive the stranded messages WITHOUT waiting for a reconnect sweep
952
960
  // (a healthy long-lived socket never re-runs onOpen): park their
@@ -971,15 +979,14 @@ async function handleParallAgentEventInner(
971
979
  }
972
980
  return effects;
973
981
  }
974
- const withChild = event as AgentEventWithChild;
975
- if (withChild.childRef) {
976
- return handleChildAgentEvent(withChild, ctx);
982
+ if (event.childRef) {
983
+ return handleChildAgentEvent(event, ctx);
977
984
  }
978
985
  const chatId = event.subject;
979
986
  try {
980
987
  // Best-effort busy classifier for fork-on-busy: only MAIN turns feed it
981
988
  // (child events took the branch above).
982
- await noteMainTurnEvent(ctx, withChild).catch(() => {});
989
+ await noteMainTurnEvent(ctx, event).catch(() => {});
983
990
  if (!chatId) return [];
984
991
  if (event.type === 'turn_completed' || event.type === 'turn_failed') {
985
992
  // Ledger close FIRST, fenced per source — deliberately NOT behind the
@@ -1065,7 +1072,7 @@ async function handleParallAgentEventInner(
1065
1072
  * turnEnvelope marker or the busy classifier.
1066
1073
  */
1067
1074
  async function handleChildAgentEvent(
1068
- event: AgentEventWithChild,
1075
+ event: AgentEvent,
1069
1076
  ctx: ConnectorContext,
1070
1077
  ): Promise<AgentEventEffect[]> {
1071
1078
  const childRef = event.childRef ?? '';
@@ -1,63 +0,0 @@
1
- import type { AgentEvent, ChannelEnvelope, ConnectorEffect } from '@parel/plugin-sdk';
2
- /**
3
- * Local mirrors of parel host types that shipped in parel-mono #140 (F3
4
- * injectInFlight / F4 child sessions) but are not yet published in
5
- * @parel/plugin-sdk (the mirror lives on parel-oss branch
6
- * feat/channel-child-sessions). The host accepts these shapes today — prod
7
- * runs #140 — so the connector uses them through this compat layer and casts
8
- * at the hook boundary.
9
- *
10
- * TODO(parel-sdk-#140-types): DELETE this file (and the casts) once
11
- * @parel/plugin-sdk ships the mirror sitting on parel-oss branch
12
- * `feat/channel-child-sessions` — check the branch/`@parel/plugin-sdk`
13
- * changelog on every sdk version bump. Until then the shapes below must stay
14
- * byte-compatible with parel-mono ts/packages/cloudflare/src/channel-types.ts.
15
- */
16
- /**
17
- * Spawn a fork child session off this connection's main conversation session.
18
- * Idempotent on childRef (opaque, connector-owned). Host gates: binding must
19
- * opt in (childSessions) + `main` routing; failures come back asynchronously
20
- * as a `child_spawn_failed` agent event. The child seeds from the parent
21
- * transcript at the last turn boundary (never sees in-flight output).
22
- */
23
- export interface SpawnChildSessionEffect {
24
- type: 'spawnChildSession';
25
- childRef: string;
26
- input: string;
27
- subject?: string;
28
- }
29
- /** emitEvent with the #140 deliverTo extension: route to a spawned child. */
30
- export interface EmitToChildEffect {
31
- type: 'emitEvent';
32
- event: ChannelEnvelope;
33
- deliverTo: {
34
- childRef: string;
35
- };
36
- }
37
- /**
38
- * Asynchronous error channel for a failed spawnChildSession effect (pushed
39
- * regardless of observe scopes). codes: disabled | unsupported_routing |
40
- * no_binding | invalid_request | depth_limit | concurrency_limit |
41
- * spawn_failed.
42
- */
43
- export interface ChildSpawnFailedEvent {
44
- type: 'child_spawn_failed';
45
- childRef: string;
46
- code: string;
47
- error: string;
48
- }
49
- /** Effect union the #140 host actually executes. */
50
- export type ExtendedConnectorEffect = ConnectorEffect | SpawnChildSessionEffect | EmitToChildEffect;
51
- /**
52
- * Every event from a connector-spawned child carries the childRef it was
53
- * spawned with (#140: AgentEventBase.childRef). Absent on main-session
54
- * events and on sdk versions that predate the field.
55
- */
56
- export type AgentEventWithChild = AgentEvent & {
57
- childRef?: string;
58
- };
59
- /** Narrow an incoming agent event to the not-yet-published failure shape. */
60
- export declare function asChildSpawnFailed(event: unknown): ChildSpawnFailedEvent | null;
61
- /** Cast helper: the host executes the extended union; the sdk type lags it. */
62
- export declare function asConnectorEffects(effects: ExtendedConnectorEffect[]): ConnectorEffect[];
63
- //# sourceMappingURL=parel-sdk-compat.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"parel-sdk-compat.d.ts","sourceRoot":"","sources":["../src/parel-sdk-compat.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAEtF;;;;;;;;;;;;;GAaG;AAEH;;;;;;GAMG;AACH,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,6EAA6E;AAC7E,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,WAAW,CAAC;IAClB,KAAK,EAAE,eAAe,CAAC;IACvB,SAAS,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;CACjC;AAED;;;;;GAKG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,EAAE,oBAAoB,CAAC;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,oDAAoD;AACpD,MAAM,MAAM,uBAAuB,GAAG,eAAe,GAAG,uBAAuB,GAAG,iBAAiB,CAAC;AAEpG;;;;GAIG;AACH,MAAM,MAAM,mBAAmB,GAAG,UAAU,GAAG;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAErE,6EAA6E;AAC7E,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,OAAO,GAAG,qBAAqB,GAAG,IAAI,CAS/E;AAED,+EAA+E;AAC/E,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,uBAAuB,EAAE,GAAG,eAAe,EAAE,CAExF"}
@@ -1,16 +0,0 @@
1
- /** Narrow an incoming agent event to the not-yet-published failure shape. */
2
- export function asChildSpawnFailed(event) {
3
- const e = event;
4
- if (e?.type !== 'child_spawn_failed')
5
- return null;
6
- return {
7
- type: 'child_spawn_failed',
8
- childRef: typeof e.childRef === 'string' ? e.childRef : '',
9
- code: typeof e.code === 'string' ? e.code : 'unknown',
10
- error: typeof e.error === 'string' ? e.error : '',
11
- };
12
- }
13
- /** Cast helper: the host executes the extended union; the sdk type lags it. */
14
- export function asConnectorEffects(effects) {
15
- return effects;
16
- }
@@ -1,77 +0,0 @@
1
- import type { AgentEvent, ChannelEnvelope, ConnectorEffect } from '@parel/plugin-sdk';
2
-
3
- /**
4
- * Local mirrors of parel host types that shipped in parel-mono #140 (F3
5
- * injectInFlight / F4 child sessions) but are not yet published in
6
- * @parel/plugin-sdk (the mirror lives on parel-oss branch
7
- * feat/channel-child-sessions). The host accepts these shapes today — prod
8
- * runs #140 — so the connector uses them through this compat layer and casts
9
- * at the hook boundary.
10
- *
11
- * TODO(parel-sdk-#140-types): DELETE this file (and the casts) once
12
- * @parel/plugin-sdk ships the mirror sitting on parel-oss branch
13
- * `feat/channel-child-sessions` — check the branch/`@parel/plugin-sdk`
14
- * changelog on every sdk version bump. Until then the shapes below must stay
15
- * byte-compatible with parel-mono ts/packages/cloudflare/src/channel-types.ts.
16
- */
17
-
18
- /**
19
- * Spawn a fork child session off this connection's main conversation session.
20
- * Idempotent on childRef (opaque, connector-owned). Host gates: binding must
21
- * opt in (childSessions) + `main` routing; failures come back asynchronously
22
- * as a `child_spawn_failed` agent event. The child seeds from the parent
23
- * transcript at the last turn boundary (never sees in-flight output).
24
- */
25
- export interface SpawnChildSessionEffect {
26
- type: 'spawnChildSession';
27
- childRef: string;
28
- input: string;
29
- subject?: string;
30
- }
31
-
32
- /** emitEvent with the #140 deliverTo extension: route to a spawned child. */
33
- export interface EmitToChildEffect {
34
- type: 'emitEvent';
35
- event: ChannelEnvelope;
36
- deliverTo: { childRef: string };
37
- }
38
-
39
- /**
40
- * Asynchronous error channel for a failed spawnChildSession effect (pushed
41
- * regardless of observe scopes). codes: disabled | unsupported_routing |
42
- * no_binding | invalid_request | depth_limit | concurrency_limit |
43
- * spawn_failed.
44
- */
45
- export interface ChildSpawnFailedEvent {
46
- type: 'child_spawn_failed';
47
- childRef: string;
48
- code: string;
49
- error: string;
50
- }
51
-
52
- /** Effect union the #140 host actually executes. */
53
- export type ExtendedConnectorEffect = ConnectorEffect | SpawnChildSessionEffect | EmitToChildEffect;
54
-
55
- /**
56
- * Every event from a connector-spawned child carries the childRef it was
57
- * spawned with (#140: AgentEventBase.childRef). Absent on main-session
58
- * events and on sdk versions that predate the field.
59
- */
60
- export type AgentEventWithChild = AgentEvent & { childRef?: string };
61
-
62
- /** Narrow an incoming agent event to the not-yet-published failure shape. */
63
- export function asChildSpawnFailed(event: unknown): ChildSpawnFailedEvent | null {
64
- const e = event as { type?: unknown; childRef?: unknown; code?: unknown; error?: unknown };
65
- if (e?.type !== 'child_spawn_failed') return null;
66
- return {
67
- type: 'child_spawn_failed',
68
- childRef: typeof e.childRef === 'string' ? e.childRef : '',
69
- code: typeof e.code === 'string' ? e.code : 'unknown',
70
- error: typeof e.error === 'string' ? e.error : '',
71
- };
72
- }
73
-
74
- /** Cast helper: the host executes the extended union; the sdk type lags it. */
75
- export function asConnectorEffects(effects: ExtendedConnectorEffect[]): ConnectorEffect[] {
76
- return effects as ConnectorEffect[];
77
- }