@vellumai/assistant 0.3.18 → 0.3.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +155 -15
- package/Dockerfile +1 -0
- package/README.md +40 -4
- package/docs/architecture/integrations.md +7 -11
- package/docs/architecture/security.md +80 -0
- package/package.json +1 -1
- package/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap +58 -0
- package/src/__tests__/approval-primitive.test.ts +540 -0
- package/src/__tests__/assistant-feature-flag-guard.test.ts +206 -0
- package/src/__tests__/assistant-feature-flag-guardrails.test.ts +198 -0
- package/src/__tests__/assistant-feature-flags-integration.test.ts +272 -0
- package/src/__tests__/call-controller.test.ts +605 -104
- package/src/__tests__/channel-invite-transport.test.ts +264 -0
- package/src/__tests__/checker.test.ts +60 -0
- package/src/__tests__/cli.test.ts +42 -1
- package/src/__tests__/config-schema.test.ts +11 -127
- package/src/__tests__/config-watcher.test.ts +0 -8
- package/src/__tests__/daemon-lifecycle.test.ts +1 -0
- package/src/__tests__/daemon-server-session-init.test.ts +8 -2
- package/src/__tests__/diff.test.ts +22 -0
- package/src/__tests__/guardian-action-copy-generator.test.ts +5 -0
- package/src/__tests__/guardian-action-grant-mint-consume.test.ts +779 -0
- package/src/__tests__/guardian-action-late-reply.test.ts +546 -1
- package/src/__tests__/guardian-actions-endpoint.test.ts +774 -0
- package/src/__tests__/guardian-control-plane-policy.test.ts +36 -3
- package/src/__tests__/guardian-dispatch.test.ts +185 -1
- package/src/__tests__/guardian-grant-minting.test.ts +532 -0
- package/src/__tests__/inbound-invite-redemption.test.ts +367 -0
- package/src/__tests__/invite-redemption-service.test.ts +306 -0
- package/src/__tests__/ipc-snapshot.test.ts +58 -0
- package/src/__tests__/notification-decision-fallback.test.ts +88 -0
- package/src/__tests__/remote-skill-policy.test.ts +215 -0
- package/src/__tests__/sandbox-diagnostics.test.ts +6 -249
- package/src/__tests__/sandbox-host-parity.test.ts +6 -13
- package/src/__tests__/scoped-approval-grants.test.ts +521 -0
- package/src/__tests__/scoped-grant-security-matrix.test.ts +444 -0
- package/src/__tests__/script-proxy-session-manager.test.ts +1 -19
- package/src/__tests__/session-load-history-repair.test.ts +169 -2
- package/src/__tests__/session-runtime-assembly.test.ts +33 -5
- package/src/__tests__/skill-feature-flags-integration.test.ts +171 -0
- package/src/__tests__/skill-feature-flags.test.ts +188 -0
- package/src/__tests__/skill-load-feature-flag.test.ts +141 -0
- package/src/__tests__/skill-mirror-parity.test.ts +1 -0
- package/src/__tests__/skill-projection-feature-flag.test.ts +363 -0
- package/src/__tests__/system-prompt.test.ts +1 -1
- package/src/__tests__/terminal-sandbox.test.ts +142 -9
- package/src/__tests__/terminal-tools.test.ts +2 -93
- package/src/__tests__/thread-seed-composer.test.ts +18 -0
- package/src/__tests__/tool-approval-handler.test.ts +350 -0
- package/src/__tests__/trust-store.test.ts +2 -0
- package/src/__tests__/trusted-contact-lifecycle-notifications.test.ts +8 -10
- package/src/__tests__/voice-scoped-grant-consumer.test.ts +533 -0
- package/src/agent/loop.ts +36 -1
- package/src/approvals/approval-primitive.ts +381 -0
- package/src/approvals/guardian-decision-primitive.ts +191 -0
- package/src/calls/call-controller.ts +276 -212
- package/src/calls/call-domain.ts +56 -6
- package/src/calls/guardian-dispatch.ts +56 -0
- package/src/calls/relay-server.ts +13 -0
- package/src/calls/types.ts +1 -1
- package/src/calls/voice-session-bridge.ts +59 -4
- package/src/cli/core-commands.ts +0 -4
- package/src/cli.ts +76 -34
- package/src/config/__tests__/feature-flag-registry-guard.test.ts +179 -0
- package/src/config/assistant-feature-flags.ts +162 -0
- package/src/config/bundled-skills/api-mapping/icon.svg +18 -0
- package/src/config/bundled-skills/messaging/TOOLS.json +30 -0
- package/src/config/bundled-skills/messaging/tools/slack-delete-message.ts +24 -0
- package/src/config/bundled-skills/notifications/SKILL.md +18 -0
- package/src/config/bundled-skills/reminder/SKILL.md +49 -2
- package/src/config/bundled-skills/time-based-actions/SKILL.md +49 -2
- package/src/config/bundled-skills/voice-setup/SKILL.md +122 -0
- package/src/config/core-schema.ts +1 -1
- package/src/config/env-registry.ts +10 -0
- package/src/config/feature-flag-registry.json +61 -0
- package/src/config/loader.ts +22 -1
- package/src/config/sandbox-schema.ts +0 -39
- package/src/config/schema.ts +12 -2
- package/src/config/skill-state.ts +34 -0
- package/src/config/skills-schema.ts +26 -0
- package/src/config/skills.ts +9 -0
- package/src/config/system-prompt.ts +110 -46
- package/src/config/templates/SOUL.md +1 -1
- package/src/config/types.ts +19 -1
- package/src/config/vellum-skills/catalog.json +1 -1
- package/src/config/vellum-skills/guardian-verify-setup/SKILL.md +1 -0
- package/src/config/vellum-skills/sms-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/telegram-setup/SKILL.md +1 -1
- package/src/config/vellum-skills/trusted-contacts/SKILL.md +104 -3
- package/src/config/vellum-skills/twilio-setup/SKILL.md +1 -1
- package/src/daemon/config-watcher.ts +0 -1
- package/src/daemon/daemon-control.ts +1 -1
- package/src/daemon/guardian-invite-intent.ts +124 -0
- package/src/daemon/handlers/avatar.ts +68 -0
- package/src/daemon/handlers/browser.ts +2 -2
- package/src/daemon/handlers/config-channels.ts +18 -0
- package/src/daemon/handlers/guardian-actions.ts +120 -0
- package/src/daemon/handlers/index.ts +4 -0
- package/src/daemon/handlers/sessions.ts +19 -0
- package/src/daemon/handlers/shared.ts +3 -1
- package/src/daemon/handlers/skills.ts +45 -2
- package/src/daemon/install-cli-launchers.ts +58 -13
- package/src/daemon/ipc-contract/guardian-actions.ts +53 -0
- package/src/daemon/ipc-contract/sessions.ts +8 -2
- package/src/daemon/ipc-contract/settings.ts +25 -2
- package/src/daemon/ipc-contract/skills.ts +1 -0
- package/src/daemon/ipc-contract-inventory.json +10 -0
- package/src/daemon/ipc-contract.ts +4 -0
- package/src/daemon/lifecycle.ts +6 -2
- package/src/daemon/main.ts +1 -0
- package/src/daemon/server.ts +1 -0
- package/src/daemon/session-lifecycle.ts +52 -7
- package/src/daemon/session-memory.ts +45 -0
- package/src/daemon/session-process.ts +260 -422
- package/src/daemon/session-runtime-assembly.ts +12 -0
- package/src/daemon/session-skill-tools.ts +14 -1
- package/src/daemon/session-tool-setup.ts +5 -0
- package/src/daemon/session.ts +11 -0
- package/src/daemon/tool-side-effects.ts +35 -9
- package/src/index.ts +0 -2
- package/src/memory/conversation-display-order-migration.ts +44 -0
- package/src/memory/conversation-queries.ts +2 -0
- package/src/memory/conversation-store.ts +91 -0
- package/src/memory/db-init.ts +13 -1
- package/src/memory/embedding-local.ts +22 -8
- package/src/memory/guardian-action-store.ts +133 -2
- package/src/memory/guardian-verification.ts +1 -1
- package/src/memory/ingress-invite-store.ts +95 -1
- package/src/memory/migrations/033-scoped-approval-grants.ts +51 -0
- package/src/memory/migrations/034-guardian-action-tool-metadata.ts +12 -0
- package/src/memory/migrations/035-guardian-action-supersession.ts +23 -0
- package/src/memory/migrations/index.ts +3 -0
- package/src/memory/schema.ts +35 -1
- package/src/memory/scoped-approval-grants.ts +518 -0
- package/src/messaging/providers/slack/client.ts +12 -0
- package/src/messaging/providers/slack/types.ts +5 -0
- package/src/notifications/decision-engine.ts +49 -12
- package/src/notifications/emit-signal.ts +7 -0
- package/src/notifications/signal.ts +7 -0
- package/src/notifications/thread-seed-composer.ts +2 -1
- package/src/permissions/checker.ts +27 -0
- package/src/runtime/channel-approval-types.ts +16 -6
- package/src/runtime/channel-approvals.ts +19 -15
- package/src/runtime/channel-invite-transport.ts +85 -0
- package/src/runtime/channel-invite-transports/telegram.ts +105 -0
- package/src/runtime/guardian-action-grant-minter.ts +154 -0
- package/src/runtime/guardian-action-message-composer.ts +30 -0
- package/src/runtime/guardian-decision-types.ts +91 -0
- package/src/runtime/http-server.ts +23 -1
- package/src/runtime/ingress-service.ts +22 -0
- package/src/runtime/invite-redemption-service.ts +181 -0
- package/src/runtime/invite-redemption-templates.ts +39 -0
- package/src/runtime/routes/call-routes.ts +2 -1
- package/src/runtime/routes/guardian-action-routes.ts +206 -0
- package/src/runtime/routes/guardian-approval-interception.ts +66 -74
- package/src/runtime/routes/inbound-message-handler.ts +568 -409
- package/src/runtime/routes/pairing-routes.ts +4 -0
- package/src/security/encrypted-store.ts +31 -17
- package/src/security/keychain.ts +176 -2
- package/src/security/secure-keys.ts +97 -0
- package/src/security/tool-approval-digest.ts +67 -0
- package/src/skills/remote-skill-policy.ts +131 -0
- package/src/tools/browser/browser-execution.ts +2 -2
- package/src/tools/browser/browser-manager.ts +46 -32
- package/src/tools/browser/browser-screencast.ts +2 -2
- package/src/tools/calls/call-start.ts +1 -1
- package/src/tools/executor.ts +22 -17
- package/src/tools/network/script-proxy/session-manager.ts +1 -5
- package/src/tools/skills/load.ts +22 -8
- package/src/tools/system/avatar-generator.ts +119 -0
- package/src/tools/system/navigate-settings.ts +65 -0
- package/src/tools/system/open-system-settings.ts +75 -0
- package/src/tools/system/voice-config.ts +121 -32
- package/src/tools/terminal/backends/native.ts +40 -19
- package/src/tools/terminal/backends/types.ts +3 -3
- package/src/tools/terminal/parser.ts +1 -1
- package/src/tools/terminal/sandbox-diagnostics.ts +6 -87
- package/src/tools/terminal/sandbox.ts +1 -12
- package/src/tools/terminal/shell.ts +3 -31
- package/src/tools/tool-approval-handler.ts +141 -3
- package/src/tools/tool-manifest.ts +6 -0
- package/src/tools/types.ts +6 -0
- package/src/util/diff.ts +36 -13
- package/Dockerfile.sandbox +0 -5
- package/src/__tests__/doordash-client.test.ts +0 -187
- package/src/__tests__/doordash-session.test.ts +0 -154
- package/src/__tests__/signup-e2e.test.ts +0 -354
- package/src/__tests__/terminal-sandbox-docker.test.ts +0 -1065
- package/src/__tests__/terminal-sandbox.integration.test.ts +0 -180
- package/src/cli/doordash.ts +0 -1057
- package/src/config/bundled-skills/doordash/SKILL.md +0 -163
- package/src/config/templates/LOOKS.md +0 -25
- package/src/doordash/cart-queries.ts +0 -787
- package/src/doordash/client.ts +0 -1016
- package/src/doordash/order-queries.ts +0 -85
- package/src/doordash/queries.ts +0 -13
- package/src/doordash/query-extractor.ts +0 -94
- package/src/doordash/search-queries.ts +0 -203
- package/src/doordash/session.ts +0 -84
- package/src/doordash/store-queries.ts +0 -246
- package/src/doordash/types.ts +0 -367
- package/src/tools/terminal/backends/docker.ts +0 -379
|
@@ -101,6 +101,19 @@ const WRAPPER_PROGRAMS = new Set([
|
|
|
101
101
|
// value of -u) as the wrapped program instead of `echo`.
|
|
102
102
|
const ENV_VALUE_FLAGS = new Set(['-u', '--unset', '-C', '--chdir']);
|
|
103
103
|
|
|
104
|
+
// Bare filenames that `rm` is allowed to delete at Medium risk (instead of
|
|
105
|
+
// High) so workspace-scoped allow rules can approve them without the
|
|
106
|
+
// dangerous `allowHighRisk` flag. Only matches when the args contain no
|
|
107
|
+
// flags and exactly one of these filenames.
|
|
108
|
+
const RM_SAFE_BARE_FILES = new Set(['BOOTSTRAP.md', 'UPDATES.md']);
|
|
109
|
+
|
|
110
|
+
function isRmOfKnownSafeFile(args: string[]): boolean {
|
|
111
|
+
if (args.length !== 1) return false;
|
|
112
|
+
const target = args[0];
|
|
113
|
+
if (target.startsWith('-') || target.includes('/')) return false;
|
|
114
|
+
return RM_SAFE_BARE_FILES.has(target);
|
|
115
|
+
}
|
|
116
|
+
|
|
104
117
|
/**
|
|
105
118
|
* Given a segment whose program is a known wrapper, return the first
|
|
106
119
|
* non-flag argument (i.e. the wrapped program name). Returns `undefined`
|
|
@@ -385,6 +398,13 @@ async function classifyRiskUncached(toolName: string, input: Record<string, unkn
|
|
|
385
398
|
if (HIGH_RISK_PROGRAMS.has(prog)) return RiskLevel.High;
|
|
386
399
|
|
|
387
400
|
if (prog === 'rm') {
|
|
401
|
+
// `rm` of known safe workspace files (no flags, bare filename) is
|
|
402
|
+
// Medium rather than High so scope-limited allow rules can approve
|
|
403
|
+
// it without needing allowHighRisk, which would bypass path checks.
|
|
404
|
+
if (isRmOfKnownSafeFile(seg.args)) {
|
|
405
|
+
maxRisk = RiskLevel.Medium;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
388
408
|
return RiskLevel.High;
|
|
389
409
|
}
|
|
390
410
|
|
|
@@ -402,7 +422,14 @@ async function classifyRiskUncached(toolName: string, input: Record<string, unkn
|
|
|
402
422
|
}
|
|
403
423
|
|
|
404
424
|
if (WRAPPER_PROGRAMS.has(prog)) {
|
|
425
|
+
// `command -v` and `command -V` are read-only lookups (print where
|
|
426
|
+
// a command lives) — don't escalate to high risk for those.
|
|
427
|
+
if (prog === 'command' && seg.args.length > 0 && (seg.args[0] === '-v' || seg.args[0] === '-V')) {
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
405
430
|
const wrapped = getWrappedProgram(seg);
|
|
431
|
+
if (wrapped === 'rm') return RiskLevel.High;
|
|
432
|
+
if (wrapped && HIGH_RISK_PROGRAMS.has(wrapped)) return RiskLevel.High;
|
|
406
433
|
if (wrapped === 'curl' || wrapped === 'wget') {
|
|
407
434
|
maxRisk = RiskLevel.Medium;
|
|
408
435
|
continue;
|
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
* same approval flow can be reused across transports.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type { GuardianDecisionAction } from './guardian-decision-types.js';
|
|
11
|
+
|
|
10
12
|
// ---------------------------------------------------------------------------
|
|
11
13
|
// Approval actions
|
|
12
14
|
// ---------------------------------------------------------------------------
|
|
@@ -20,12 +22,20 @@ export interface ApprovalActionOption {
|
|
|
20
22
|
label: string;
|
|
21
23
|
}
|
|
22
24
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
25
|
+
/**
|
|
26
|
+
* Map `GuardianDecisionAction[]` to `ApprovalActionOption[]` so channel
|
|
27
|
+
* prompt payloads can be derived from the unified decision action set.
|
|
28
|
+
* The `action` field from GuardianDecisionAction maps to the `id` field
|
|
29
|
+
* on ApprovalActionOption (both are canonical action identifiers).
|
|
30
|
+
*/
|
|
31
|
+
export function toApprovalActionOptions(
|
|
32
|
+
actions: GuardianDecisionAction[],
|
|
33
|
+
): ApprovalActionOption[] {
|
|
34
|
+
return actions.map(a => ({
|
|
35
|
+
id: a.action as ApprovalAction,
|
|
36
|
+
label: a.label,
|
|
37
|
+
}));
|
|
38
|
+
}
|
|
29
39
|
|
|
30
40
|
// ---------------------------------------------------------------------------
|
|
31
41
|
// Approval prompt
|
|
@@ -17,7 +17,8 @@ import type {
|
|
|
17
17
|
ApprovalUIMetadata,
|
|
18
18
|
ChannelApprovalPrompt,
|
|
19
19
|
} from './channel-approval-types.js';
|
|
20
|
-
import {
|
|
20
|
+
import { toApprovalActionOptions } from './channel-approval-types.js';
|
|
21
|
+
import { buildDecisionActions, buildPlainTextFallback } from './guardian-decision-types.js';
|
|
21
22
|
import * as pendingInteractions from './pending-interactions.js';
|
|
22
23
|
|
|
23
24
|
/** Summary of a pending interaction, used by channel approval flows. */
|
|
@@ -69,6 +70,11 @@ export function getApprovalInfoByConversation(conversationId: string): PendingAp
|
|
|
69
70
|
|
|
70
71
|
/**
|
|
71
72
|
* Internal helper: turn a PendingApprovalInfo into a ChannelApprovalPrompt.
|
|
73
|
+
*
|
|
74
|
+
* Derives actions from the shared `buildDecisionActions` builder defined in
|
|
75
|
+
* guardian-decision-types.ts, then maps them to the channel-facing
|
|
76
|
+
* `ApprovalActionOption` shape. This ensures channel button sets are always
|
|
77
|
+
* consistent with the unified `GuardianDecisionPrompt` type.
|
|
72
78
|
*/
|
|
73
79
|
function buildPromptFromApprovalInfo(info: PendingApprovalInfo): ChannelApprovalPrompt {
|
|
74
80
|
const promptText = composeApprovalMessage({
|
|
@@ -76,15 +82,11 @@ function buildPromptFromApprovalInfo(info: PendingApprovalInfo): ChannelApproval
|
|
|
76
82
|
toolName: info.toolName,
|
|
77
83
|
});
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// Plain-text fallback must remain parser-compatible (contains "yes"/"always"/"no" keywords).
|
|
85
|
-
const plainTextFallback = info.persistentDecisionsAllowed === false
|
|
86
|
-
? `${promptText}\n\nReply "yes" to approve or "no" to reject.`
|
|
87
|
-
: `${promptText}\n\nReply "yes" to approve once, "always" to approve always, or "no" to reject.`;
|
|
85
|
+
const decisionActions = buildDecisionActions({
|
|
86
|
+
persistentDecisionsAllowed: info.persistentDecisionsAllowed,
|
|
87
|
+
});
|
|
88
|
+
const actions = toApprovalActionOptions(decisionActions);
|
|
89
|
+
const plainTextFallback = buildPlainTextFallback(promptText, decisionActions);
|
|
88
90
|
|
|
89
91
|
return { promptText, actions, plainTextFallback };
|
|
90
92
|
}
|
|
@@ -199,6 +201,10 @@ export function handleChannelDecision(
|
|
|
199
201
|
* Build an approval prompt that includes context about which non-guardian
|
|
200
202
|
* user is requesting the action. Sent to the guardian's chat so they
|
|
201
203
|
* can approve or deny on behalf of the requester.
|
|
204
|
+
*
|
|
205
|
+
* Uses the shared `buildDecisionActions` builder with `forGuardianOnBehalf`
|
|
206
|
+
* set to true, which excludes `approve_always` since guardians cannot
|
|
207
|
+
* permanently allowlist tools on behalf of requesters.
|
|
202
208
|
*/
|
|
203
209
|
export function buildGuardianApprovalPrompt(
|
|
204
210
|
info: PendingApprovalInfo,
|
|
@@ -210,11 +216,9 @@ export function buildGuardianApprovalPrompt(
|
|
|
210
216
|
requesterIdentifier,
|
|
211
217
|
});
|
|
212
218
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
const plainTextFallback = `${promptText}\n\nReply "yes" to approve or "no" to reject.`;
|
|
219
|
+
const decisionActions = buildDecisionActions({ forGuardianOnBehalf: true });
|
|
220
|
+
const actions = toApprovalActionOptions(decisionActions);
|
|
221
|
+
const plainTextFallback = buildPlainTextFallback(promptText, decisionActions);
|
|
218
222
|
|
|
219
223
|
return { promptText, actions, plainTextFallback };
|
|
220
224
|
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Channel invite transport abstraction.
|
|
3
|
+
*
|
|
4
|
+
* Defines a transport interface for building shareable invite links and
|
|
5
|
+
* extracting inbound invite tokens from channel-specific payloads. Each
|
|
6
|
+
* channel (Telegram, SMS, Slack, etc.) registers an adapter that knows
|
|
7
|
+
* how to construct deep links and parse incoming tokens for that channel.
|
|
8
|
+
*
|
|
9
|
+
* The transport layer is intentionally thin: it handles URL construction
|
|
10
|
+
* and token extraction only. Redemption logic lives in
|
|
11
|
+
* `invite-redemption-service.ts`.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { ChannelId } from '../channels/types.js';
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Types
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface InviteSharePayload {
|
|
21
|
+
/** The full URL the recipient can open to redeem the invite. */
|
|
22
|
+
url: string;
|
|
23
|
+
/** Human-readable text suitable for display alongside the link. */
|
|
24
|
+
displayText: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ChannelInviteTransport {
|
|
28
|
+
/** The channel this transport handles. */
|
|
29
|
+
channel: ChannelId;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Build a shareable invite payload (URL + display text) from a raw token.
|
|
33
|
+
*
|
|
34
|
+
* The raw token is the base64url-encoded secret returned by
|
|
35
|
+
* `ingress-invite-store.createInvite`. The transport wraps it in a
|
|
36
|
+
* channel-specific deep link so the recipient can redeem the invite
|
|
37
|
+
* by clicking/tapping the link.
|
|
38
|
+
*/
|
|
39
|
+
buildShareableInvite(params: {
|
|
40
|
+
rawToken: string;
|
|
41
|
+
sourceChannel: ChannelId;
|
|
42
|
+
}): InviteSharePayload;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extract an invite token from an inbound channel message.
|
|
46
|
+
*
|
|
47
|
+
* Returns the raw token string (without the `iv_` prefix) if the
|
|
48
|
+
* message contains a valid invite token, or `undefined` otherwise.
|
|
49
|
+
*/
|
|
50
|
+
extractInboundToken(params: {
|
|
51
|
+
commandIntent?: Record<string, unknown>;
|
|
52
|
+
content: string;
|
|
53
|
+
sourceMetadata?: Record<string, unknown>;
|
|
54
|
+
}): string | undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// Registry
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
const registry = new Map<ChannelId, ChannelInviteTransport>();
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Register a channel invite transport. Overwrites any previously registered
|
|
65
|
+
* transport for the same channel.
|
|
66
|
+
*/
|
|
67
|
+
export function registerTransport(transport: ChannelInviteTransport): void {
|
|
68
|
+
registry.set(transport.channel, transport);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Look up the registered transport for a channel. Returns `undefined` when
|
|
73
|
+
* no transport has been registered for the given channel.
|
|
74
|
+
*/
|
|
75
|
+
export function getTransport(channel: ChannelId): ChannelInviteTransport | undefined {
|
|
76
|
+
return registry.get(channel);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Reset the registry. Intended for tests only.
|
|
81
|
+
* @internal
|
|
82
|
+
*/
|
|
83
|
+
export function _resetRegistry(): void {
|
|
84
|
+
registry.clear();
|
|
85
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram channel invite transport adapter.
|
|
3
|
+
*
|
|
4
|
+
* Builds `https://t.me/<botUsername>?start=iv_<token>` deep links and
|
|
5
|
+
* extracts invite tokens from `/start iv_<token>` command payloads.
|
|
6
|
+
*
|
|
7
|
+
* The `iv_` prefix distinguishes invite tokens from `gv_` (guardian
|
|
8
|
+
* verification) tokens that use the same `/start` deep-link mechanism.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { ChannelId } from '../../channels/types.js';
|
|
12
|
+
import { getCredentialMetadata } from '../../tools/credentials/metadata-store.js';
|
|
13
|
+
import {
|
|
14
|
+
type ChannelInviteTransport,
|
|
15
|
+
type InviteSharePayload,
|
|
16
|
+
registerTransport,
|
|
17
|
+
} from '../channel-invite-transport.js';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Bot username resolution
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the Telegram bot username from credential metadata, falling back
|
|
25
|
+
* to the TELEGRAM_BOT_USERNAME environment variable. Mirrors the resolution
|
|
26
|
+
* strategy used in `guardian-outbound-actions.ts`.
|
|
27
|
+
*/
|
|
28
|
+
function getTelegramBotUsername(): string | undefined {
|
|
29
|
+
const meta = getCredentialMetadata('telegram', 'bot_token');
|
|
30
|
+
if (meta?.accountInfo && typeof meta.accountInfo === 'string' && meta.accountInfo.trim().length > 0) {
|
|
31
|
+
return meta.accountInfo.trim();
|
|
32
|
+
}
|
|
33
|
+
return process.env.TELEGRAM_BOT_USERNAME || undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Token prefix
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
const INVITE_TOKEN_PREFIX = 'iv_';
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Transport implementation
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
export const telegramInviteTransport: ChannelInviteTransport = {
|
|
47
|
+
channel: 'telegram' as ChannelId,
|
|
48
|
+
|
|
49
|
+
buildShareableInvite(params: {
|
|
50
|
+
rawToken: string;
|
|
51
|
+
sourceChannel: ChannelId;
|
|
52
|
+
}): InviteSharePayload {
|
|
53
|
+
const botUsername = getTelegramBotUsername();
|
|
54
|
+
if (!botUsername) {
|
|
55
|
+
throw new Error('Telegram bot username is not configured. Set up the Telegram integration first.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const url = `https://t.me/${botUsername}?start=${INVITE_TOKEN_PREFIX}${params.rawToken}`;
|
|
59
|
+
return {
|
|
60
|
+
url,
|
|
61
|
+
displayText: `Open in Telegram: ${url}`,
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
extractInboundToken(params: {
|
|
66
|
+
commandIntent?: Record<string, unknown>;
|
|
67
|
+
content: string;
|
|
68
|
+
sourceMetadata?: Record<string, unknown>;
|
|
69
|
+
}): string | undefined {
|
|
70
|
+
// Primary path: structured command intent from the gateway.
|
|
71
|
+
// The gateway normalizes `/start <payload>` into
|
|
72
|
+
// `{ type: 'start', payload: '<payload>' }`.
|
|
73
|
+
if (
|
|
74
|
+
params.commandIntent &&
|
|
75
|
+
params.commandIntent.type === 'start' &&
|
|
76
|
+
typeof params.commandIntent.payload === 'string'
|
|
77
|
+
) {
|
|
78
|
+
const payload = params.commandIntent.payload;
|
|
79
|
+
if (payload.startsWith(INVITE_TOKEN_PREFIX)) {
|
|
80
|
+
const token = payload.slice(INVITE_TOKEN_PREFIX.length);
|
|
81
|
+
// Reject empty or whitespace-only tokens
|
|
82
|
+
if (token.length > 0 && token.trim().length > 0) {
|
|
83
|
+
return token;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fallback: raw content parsing for `/start iv_<token>` messages.
|
|
90
|
+
// This handles cases where the gateway forwards the raw command text
|
|
91
|
+
// without a structured commandIntent.
|
|
92
|
+
const match = params.content.match(/^\/start\s+iv_(\S+)/);
|
|
93
|
+
if (match && match[1] && match[1].length > 0) {
|
|
94
|
+
return match[1];
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return undefined;
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// ---------------------------------------------------------------------------
|
|
102
|
+
// Auto-register on import
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
|
|
105
|
+
registerTransport(telegramInviteTransport);
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper for minting scoped approval grants when a guardian-action
|
|
3
|
+
* request is resolved with tool metadata.
|
|
4
|
+
*
|
|
5
|
+
* Used by both the channel inbound path (inbound-message-handler.ts) and
|
|
6
|
+
* the desktop/IPC path (session-process.ts) to ensure grants are minted
|
|
7
|
+
* consistently regardless of which channel the guardian answers on.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { mintGrantFromDecision } from '../approvals/approval-primitive.js';
|
|
11
|
+
import type { GuardianActionRequest } from '../memory/guardian-action-store.js';
|
|
12
|
+
import { getLogger } from '../util/logger.js';
|
|
13
|
+
import { runApprovalConversationTurn } from './approval-conversation-turn.js';
|
|
14
|
+
import { parseApprovalDecision } from './channel-approval-parser.js';
|
|
15
|
+
import type { ApprovalConversationGenerator } from './http-types.js';
|
|
16
|
+
|
|
17
|
+
const log = getLogger('guardian-action-grant-minter');
|
|
18
|
+
|
|
19
|
+
/** TTL for scoped approval grants minted on guardian-action answer resolution. */
|
|
20
|
+
export const GUARDIAN_ACTION_GRANT_TTL_MS = 5 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Mint a `tool_signature` scoped grant when a guardian-action request is
|
|
24
|
+
* resolved and the request carries tool metadata (toolName + inputDigest).
|
|
25
|
+
*
|
|
26
|
+
* Uses two-tier classification:
|
|
27
|
+
* 1. Deterministic fast path via parseApprovalDecision (exact keyword match).
|
|
28
|
+
* 2. LLM fallback via runApprovalConversationTurn when the deterministic
|
|
29
|
+
* parser returns null and an approvalConversationGenerator is provided.
|
|
30
|
+
*
|
|
31
|
+
* Skips silently when:
|
|
32
|
+
* - The resolved request has no toolName/inputDigest (informational consult).
|
|
33
|
+
* - The guardian's answer is not classified as approval by either tier (fail-closed).
|
|
34
|
+
*
|
|
35
|
+
* Fails silently on error -- grant minting is best-effort and must never
|
|
36
|
+
* block the guardian-action answer flow.
|
|
37
|
+
*/
|
|
38
|
+
export async function tryMintGuardianActionGrant(params: {
|
|
39
|
+
request: GuardianActionRequest;
|
|
40
|
+
answerText: string;
|
|
41
|
+
decisionChannel: string;
|
|
42
|
+
guardianExternalUserId?: string;
|
|
43
|
+
approvalConversationGenerator?: ApprovalConversationGenerator;
|
|
44
|
+
}): Promise<void> {
|
|
45
|
+
const { request, answerText, decisionChannel, guardianExternalUserId, approvalConversationGenerator } = params;
|
|
46
|
+
|
|
47
|
+
// Only mint for requests that carry tool metadata -- informational
|
|
48
|
+
// ASK_GUARDIAN consults without tool context do not produce grants.
|
|
49
|
+
if (!request.toolName || !request.inputDigest) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Tier 1: Deterministic fast path -- try exact keyword matching first.
|
|
54
|
+
// Guardian-action invariant: grants are always one-time `tool_signature`
|
|
55
|
+
// scoped. We treat `approve_always` from the deterministic parser the
|
|
56
|
+
// same as `approve_once` -- the grant is still single-use. This keeps
|
|
57
|
+
// the guardian-action path aligned with the primary approval interception
|
|
58
|
+
// flow where guardians are limited to approve_once / reject.
|
|
59
|
+
const decision = parseApprovalDecision(answerText);
|
|
60
|
+
let isApproval = decision?.action === 'approve_once' || decision?.action === 'approve_always';
|
|
61
|
+
|
|
62
|
+
// Tier 2: LLM fallback -- when the deterministic parser found no match
|
|
63
|
+
// and a generator is available, delegate to the conversational engine.
|
|
64
|
+
// Only allow approve_once (not approve_always) to keep guardian-action
|
|
65
|
+
// grants strictly one-time and consistent with guardian policy.
|
|
66
|
+
if (!isApproval && !decision && approvalConversationGenerator) {
|
|
67
|
+
try {
|
|
68
|
+
const llmResult = await runApprovalConversationTurn(
|
|
69
|
+
{
|
|
70
|
+
toolName: request.toolName,
|
|
71
|
+
allowedActions: ['approve_once', 'reject'],
|
|
72
|
+
role: 'guardian',
|
|
73
|
+
pendingApprovals: [{ requestId: request.id, toolName: request.toolName }],
|
|
74
|
+
userMessage: answerText,
|
|
75
|
+
},
|
|
76
|
+
approvalConversationGenerator,
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
isApproval = llmResult.disposition === 'approve_once';
|
|
80
|
+
|
|
81
|
+
log.info(
|
|
82
|
+
{
|
|
83
|
+
event: 'guardian_action_grant_llm_fallback',
|
|
84
|
+
toolName: request.toolName,
|
|
85
|
+
requestId: request.id,
|
|
86
|
+
answerText,
|
|
87
|
+
llmDisposition: llmResult.disposition,
|
|
88
|
+
matched: isApproval,
|
|
89
|
+
decisionChannel,
|
|
90
|
+
},
|
|
91
|
+
`LLM fallback classifier returned disposition: ${llmResult.disposition}`,
|
|
92
|
+
);
|
|
93
|
+
} catch (err) {
|
|
94
|
+
// Fail-closed: generator errors must not produce grants.
|
|
95
|
+
log.warn(
|
|
96
|
+
{
|
|
97
|
+
event: 'guardian_action_grant_llm_fallback_error',
|
|
98
|
+
toolName: request.toolName,
|
|
99
|
+
requestId: request.id,
|
|
100
|
+
err,
|
|
101
|
+
decisionChannel,
|
|
102
|
+
},
|
|
103
|
+
'LLM fallback classifier threw an error; treating as non-approval (fail-closed)',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (!isApproval) {
|
|
109
|
+
log.info(
|
|
110
|
+
{
|
|
111
|
+
event: 'guardian_action_grant_skipped_no_approval',
|
|
112
|
+
toolName: request.toolName,
|
|
113
|
+
requestId: request.id,
|
|
114
|
+
answerText,
|
|
115
|
+
parsedAction: decision?.action ?? null,
|
|
116
|
+
decisionChannel,
|
|
117
|
+
},
|
|
118
|
+
'Skipped grant minting: guardian answer not classified as approval',
|
|
119
|
+
);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const result = mintGrantFromDecision({
|
|
124
|
+
assistantId: request.assistantId,
|
|
125
|
+
scopeMode: 'tool_signature',
|
|
126
|
+
toolName: request.toolName,
|
|
127
|
+
inputDigest: request.inputDigest,
|
|
128
|
+
requestChannel: request.sourceChannel,
|
|
129
|
+
decisionChannel,
|
|
130
|
+
executionChannel: null,
|
|
131
|
+
conversationId: request.sourceConversationId,
|
|
132
|
+
callSessionId: request.callSessionId,
|
|
133
|
+
guardianExternalUserId: guardianExternalUserId ?? null,
|
|
134
|
+
expiresAt: new Date(Date.now() + GUARDIAN_ACTION_GRANT_TTL_MS).toISOString(),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
if (result.ok) {
|
|
138
|
+
log.info(
|
|
139
|
+
{
|
|
140
|
+
event: 'guardian_action_grant_minted',
|
|
141
|
+
toolName: request.toolName,
|
|
142
|
+
requestId: request.id,
|
|
143
|
+
callSessionId: request.callSessionId,
|
|
144
|
+
decisionChannel,
|
|
145
|
+
},
|
|
146
|
+
'Minted scoped approval grant for guardian-action answer resolution',
|
|
147
|
+
);
|
|
148
|
+
} else {
|
|
149
|
+
log.error(
|
|
150
|
+
{ reason: result.reason, toolName: request.toolName, requestId: request.id },
|
|
151
|
+
'Failed to mint scoped approval grant for guardian-action (non-fatal)',
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
@@ -26,11 +26,16 @@ export type GuardianActionMessageScenario =
|
|
|
26
26
|
| 'guardian_followup_failed'
|
|
27
27
|
| 'guardian_followup_declined_ack'
|
|
28
28
|
| 'guardian_followup_clarification'
|
|
29
|
+
| 'guardian_pending_disambiguation'
|
|
29
30
|
| 'guardian_expired_disambiguation'
|
|
30
31
|
| 'guardian_followup_disambiguation'
|
|
31
32
|
| 'guardian_stale_answered'
|
|
32
33
|
| 'guardian_stale_expired'
|
|
33
34
|
| 'guardian_stale_followup'
|
|
35
|
+
| 'guardian_stale_superseded'
|
|
36
|
+
| 'guardian_superseded_remap'
|
|
37
|
+
| 'guardian_unknown_code'
|
|
38
|
+
| 'guardian_auto_matched'
|
|
34
39
|
| 'outbound_message_copy'
|
|
35
40
|
| 'followup_message_sent'
|
|
36
41
|
| 'followup_call_started'
|
|
@@ -48,6 +53,10 @@ export interface GuardianActionMessageContext {
|
|
|
48
53
|
failureReason?: string;
|
|
49
54
|
counterpartyPhone?: string;
|
|
50
55
|
requestCodes?: string[];
|
|
56
|
+
/** The code the guardian provided that was not recognized. */
|
|
57
|
+
unknownCode?: string;
|
|
58
|
+
/** The code of the active request that supersedes the one the guardian targeted. */
|
|
59
|
+
activeRequestCode?: string;
|
|
51
60
|
}
|
|
52
61
|
|
|
53
62
|
export interface ComposeGuardianActionMessageOptions {
|
|
@@ -181,6 +190,11 @@ export function getGuardianActionFallbackMessage(context: GuardianActionMessageC
|
|
|
181
190
|
case 'guardian_followup_clarification':
|
|
182
191
|
return "Sorry, I didn't quite catch that. Would you like to call them back, send them a message, or skip it for now?";
|
|
183
192
|
|
|
193
|
+
case 'guardian_pending_disambiguation':
|
|
194
|
+
return listedCodes
|
|
195
|
+
? `You have multiple pending guardian questions. Please prefix your reply with the reference code (${listedCodes}) so I know which question you're answering.`
|
|
196
|
+
: 'You have multiple pending guardian questions. Please prefix your reply with the reference code so I know which question you\'re answering.';
|
|
197
|
+
|
|
184
198
|
case 'guardian_expired_disambiguation':
|
|
185
199
|
return listedCodes
|
|
186
200
|
? `You have multiple expired guardian questions. Please prefix your reply with the reference code (${listedCodes}) so I know which question you're answering.`
|
|
@@ -200,6 +214,22 @@ export function getGuardianActionFallbackMessage(context: GuardianActionMessageC
|
|
|
200
214
|
case 'guardian_stale_followup':
|
|
201
215
|
return 'It looks like this follow-up has already been handled. No further action is needed.';
|
|
202
216
|
|
|
217
|
+
case 'guardian_stale_superseded':
|
|
218
|
+
return 'This request is no longer active. The call has ended and no further action is needed.';
|
|
219
|
+
|
|
220
|
+
case 'guardian_unknown_code':
|
|
221
|
+
return context.unknownCode
|
|
222
|
+
? `I don't recognize the code "${context.unknownCode}". Please check the reference code and try again.`
|
|
223
|
+
: "I don't recognize that reference code. Please check the code and try again.";
|
|
224
|
+
|
|
225
|
+
case 'guardian_auto_matched':
|
|
226
|
+
return 'Got it, routing your answer to the active request.';
|
|
227
|
+
|
|
228
|
+
case 'guardian_superseded_remap':
|
|
229
|
+
return context.questionText
|
|
230
|
+
? `Got it! Your answer has been applied to the current active request: "${context.questionText}"`
|
|
231
|
+
: 'Got it! Your answer has been applied to the current active request on the call.';
|
|
232
|
+
|
|
203
233
|
case 'outbound_message_copy':
|
|
204
234
|
// This SMS is sent TO the original caller relaying the guardian's answer.
|
|
205
235
|
// When lateAnswerText is available, include it — that's the whole point of message_back.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the guardian decision primitive.
|
|
3
|
+
*
|
|
4
|
+
* All decision entrypoints (callback buttons, conversational engine, legacy
|
|
5
|
+
* parser, requester self-cancel) use these types to route through the
|
|
6
|
+
* unified `applyGuardianDecision` primitive.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// Guardian decision prompt
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
/** Structured model for prompts shown to guardians. */
|
|
14
|
+
export interface GuardianDecisionPrompt {
|
|
15
|
+
requestId: string;
|
|
16
|
+
/** Short human-readable code for the request. */
|
|
17
|
+
requestCode: string;
|
|
18
|
+
state: 'pending' | 'followup_awaiting_choice' | 'expired_superseded_with_active_call';
|
|
19
|
+
questionText: string;
|
|
20
|
+
toolName: string | null;
|
|
21
|
+
actions: GuardianDecisionAction[];
|
|
22
|
+
expiresAt: number;
|
|
23
|
+
conversationId: string;
|
|
24
|
+
callSessionId: string | null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface GuardianDecisionAction {
|
|
28
|
+
/** Canonical action identifier. */
|
|
29
|
+
action: string;
|
|
30
|
+
/** Human-readable label for the action. */
|
|
31
|
+
label: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
// Shared decision action constants
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
/** Canonical set of all guardian decision actions with their labels. */
|
|
39
|
+
export const GUARDIAN_DECISION_ACTIONS = {
|
|
40
|
+
approve_once: { action: 'approve_once', label: 'Approve once' },
|
|
41
|
+
approve_always: { action: 'approve_always', label: 'Approve always' },
|
|
42
|
+
reject: { action: 'reject', label: 'Reject' },
|
|
43
|
+
} as const satisfies Record<string, GuardianDecisionAction>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Build the set of `GuardianDecisionAction` items appropriate for a prompt,
|
|
47
|
+
* respecting whether persistent decisions (approve_always) are allowed.
|
|
48
|
+
*
|
|
49
|
+
* When `persistentDecisionsAllowed` is `false`, the `approve_always` action
|
|
50
|
+
* is excluded. When `forGuardianOnBehalf` is `true` (guardian acting on behalf
|
|
51
|
+
* of a requester), `approve_always` is also excluded since guardians cannot
|
|
52
|
+
* permanently allowlist tools on behalf of others.
|
|
53
|
+
*/
|
|
54
|
+
export function buildDecisionActions(opts?: {
|
|
55
|
+
persistentDecisionsAllowed?: boolean;
|
|
56
|
+
forGuardianOnBehalf?: boolean;
|
|
57
|
+
}): GuardianDecisionAction[] {
|
|
58
|
+
const showAlways = opts?.persistentDecisionsAllowed !== false && !opts?.forGuardianOnBehalf;
|
|
59
|
+
return [
|
|
60
|
+
GUARDIAN_DECISION_ACTIONS.approve_once,
|
|
61
|
+
...(showAlways ? [GUARDIAN_DECISION_ACTIONS.approve_always] : []),
|
|
62
|
+
GUARDIAN_DECISION_ACTIONS.reject,
|
|
63
|
+
];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Build the plain-text fallback instruction string that matches the given
|
|
68
|
+
* set of decision actions. Ensures the text always includes parser-compatible
|
|
69
|
+
* keywords (yes/always/no) so text-based fallback remains actionable.
|
|
70
|
+
*/
|
|
71
|
+
export function buildPlainTextFallback(
|
|
72
|
+
promptText: string,
|
|
73
|
+
actions: GuardianDecisionAction[],
|
|
74
|
+
): string {
|
|
75
|
+
const hasAlways = actions.some(a => a.action === 'approve_always');
|
|
76
|
+
return hasAlways
|
|
77
|
+
? `${promptText}\n\nReply "yes" to approve once, "always" to approve always, or "no" to reject.`
|
|
78
|
+
: `${promptText}\n\nReply "yes" to approve or "no" to reject.`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// ---------------------------------------------------------------------------
|
|
82
|
+
// Apply decision result
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
|
|
85
|
+
export interface ApplyGuardianDecisionResult {
|
|
86
|
+
applied: boolean;
|
|
87
|
+
reason?: 'stale' | 'identity_mismatch' | 'invalid_action' | 'not_found' | 'expired';
|
|
88
|
+
requestId?: string;
|
|
89
|
+
/** Feedback text when the action was parsed from user text. */
|
|
90
|
+
userText?: string;
|
|
91
|
+
}
|