@vellumai/credential-executor 0.10.4 → 0.10.5-dev.202607022329.d89bf2f
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/node_modules/@vellumai/service-contracts/src/__tests__/ingress.test.ts +0 -8
- package/node_modules/@vellumai/service-contracts/src/__tests__/package-boundary.test.ts +1 -5
- package/node_modules/@vellumai/service-contracts/src/twilio-ingress.ts +3 -14
- package/package.json +1 -1
- package/src/__tests__/grant-store.test.ts +104 -0
- package/src/grants/temporary-store.ts +71 -8
- package/src/managed-main.ts +18 -21
|
@@ -5,10 +5,8 @@ import {
|
|
|
5
5
|
normalizePublicBaseUrl,
|
|
6
6
|
} from "../ingress.js";
|
|
7
7
|
import {
|
|
8
|
-
buildTwilioConnectActionUrl,
|
|
9
8
|
buildTwilioMediaStreamUrl,
|
|
10
9
|
buildTwilioPhoneNumberWebhookUrls,
|
|
11
|
-
buildTwilioRelayUrl,
|
|
12
10
|
buildTwilioVoiceWebhookUrl,
|
|
13
11
|
resolveTwilioPublicBaseUrl,
|
|
14
12
|
} from "../twilio-ingress.js";
|
|
@@ -90,12 +88,6 @@ describe("Twilio ingress helpers", () => {
|
|
|
90
88
|
expect(buildTwilioVoiceWebhookUrl("https://example.test", "call-123")).toBe(
|
|
91
89
|
"https://example.test/webhooks/twilio/voice?callSessionId=call-123",
|
|
92
90
|
);
|
|
93
|
-
expect(buildTwilioConnectActionUrl("https://example.test")).toBe(
|
|
94
|
-
"https://example.test/webhooks/twilio/connect-action",
|
|
95
|
-
);
|
|
96
|
-
expect(buildTwilioRelayUrl("https://example.test")).toBe(
|
|
97
|
-
"wss://example.test/webhooks/twilio/relay",
|
|
98
|
-
);
|
|
99
91
|
expect(buildTwilioMediaStreamUrl("http://example.test")).toBe(
|
|
100
92
|
"ws://example.test/webhooks/twilio/media-stream",
|
|
101
93
|
);
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
* 1. Does NOT import from assistant, gateway, credential-executor, or other
|
|
6
6
|
* service runtime modules.
|
|
7
7
|
* 2. Does NOT import from runtime shared packages that sit above it in the
|
|
8
|
-
* dependency hierarchy (@vellumai/credential-storage, @vellumai/egress-proxy
|
|
9
|
-
* @vellumai/skill-host-contracts).
|
|
8
|
+
* dependency hierarchy (@vellumai/credential-storage, @vellumai/egress-proxy).
|
|
10
9
|
* 3. Does NOT import from x-client packages (@vellumai/assistant-client,
|
|
11
10
|
* @vellumai/ces-client, @vellumai/gateway-client).
|
|
12
11
|
* 4. Remains a pure schema/type package — no runtime dependencies beyond zod.
|
|
@@ -73,8 +72,6 @@ const FORBIDDEN_IMPORT_PATTERNS = [
|
|
|
73
72
|
/require\s*\(\s*["']@vellumai\/credential-storage(?:\/|["'])/,
|
|
74
73
|
/from\s+["']@vellumai\/egress-proxy(?:\/|["'])/,
|
|
75
74
|
/require\s*\(\s*["']@vellumai\/egress-proxy(?:\/|["'])/,
|
|
76
|
-
/from\s+["']@vellumai\/skill-host-contracts(?:\/|["'])/,
|
|
77
|
-
/require\s*\(\s*["']@vellumai\/skill-host-contracts(?:\/|["'])/,
|
|
78
75
|
|
|
79
76
|
// x-client packages (higher layer)
|
|
80
77
|
/from\s+["']@vellumai\/assistant-client(?:\/|["'])/,
|
|
@@ -143,7 +140,6 @@ describe("package boundary", () => {
|
|
|
143
140
|
"@vellumai/assistant",
|
|
144
141
|
"@vellumai/credential-storage",
|
|
145
142
|
"@vellumai/egress-proxy",
|
|
146
|
-
"@vellumai/skill-host-contracts",
|
|
147
143
|
"@vellumai/assistant-client",
|
|
148
144
|
"@vellumai/ces-client",
|
|
149
145
|
"@vellumai/gateway-client",
|
|
@@ -2,9 +2,6 @@ import { normalizePublicBaseUrl } from "./ingress.js";
|
|
|
2
2
|
|
|
3
3
|
export const TWILIO_VOICE_WEBHOOK_PATH = "/webhooks/twilio/voice";
|
|
4
4
|
export const TWILIO_STATUS_WEBHOOK_PATH = "/webhooks/twilio/status";
|
|
5
|
-
export const TWILIO_CONNECT_ACTION_WEBHOOK_PATH =
|
|
6
|
-
"/webhooks/twilio/connect-action";
|
|
7
|
-
export const TWILIO_RELAY_WEBHOOK_PATH = "/webhooks/twilio/relay";
|
|
8
5
|
export const TWILIO_MEDIA_STREAM_WEBHOOK_PATH = "/webhooks/twilio/media-stream";
|
|
9
6
|
|
|
10
7
|
/**
|
|
@@ -13,9 +10,9 @@ export const TWILIO_MEDIA_STREAM_WEBHOOK_PATH = "/webhooks/twilio/media-stream";
|
|
|
13
10
|
* with the actual public URL (from Velay registration, config, or the
|
|
14
11
|
* `X-Vellum-Ingress-URL` header) before returning TwiML to Twilio.
|
|
15
12
|
*
|
|
16
|
-
* The placeholder uses `https://` so that `
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* The placeholder uses `https://` so that `buildTwilioMediaStreamUrl` can
|
|
14
|
+
* apply the standard `http→ws` scheme conversion, producing
|
|
15
|
+
* `wss://__VELLUM_PUBLIC_BASE_URL__/…` in the output.
|
|
19
16
|
*/
|
|
20
17
|
export const TWILIO_PUBLIC_BASE_URL_PLACEHOLDER =
|
|
21
18
|
"https://__VELLUM_PUBLIC_BASE_URL__";
|
|
@@ -58,14 +55,6 @@ export function buildTwilioStatusWebhookUrl(baseUrl: string): string {
|
|
|
58
55
|
return `${baseUrl}${TWILIO_STATUS_WEBHOOK_PATH}`;
|
|
59
56
|
}
|
|
60
57
|
|
|
61
|
-
export function buildTwilioConnectActionUrl(baseUrl: string): string {
|
|
62
|
-
return `${baseUrl}${TWILIO_CONNECT_ACTION_WEBHOOK_PATH}`;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function buildTwilioRelayUrl(baseUrl: string): string {
|
|
66
|
-
return `${toTwilioWebSocketBaseUrl(baseUrl)}${TWILIO_RELAY_WEBHOOK_PATH}`;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
58
|
export function buildTwilioMediaStreamUrl(baseUrl: string): string {
|
|
70
59
|
return `${toTwilioWebSocketBaseUrl(baseUrl)}${TWILIO_MEDIA_STREAM_WEBHOOK_PATH}`;
|
|
71
60
|
}
|
package/package.json
CHANGED
|
@@ -419,6 +419,110 @@ describe("TemporaryGrantStore", () => {
|
|
|
419
419
|
expect(store.check("allow_once", "hash-1")).toBe(false);
|
|
420
420
|
expect(store.check("allow_once", "hash-2")).toBe(false);
|
|
421
421
|
});
|
|
422
|
+
|
|
423
|
+
test("remains usable immediately (default TTL is not instantaneous)", () => {
|
|
424
|
+
// The default `allow_once` TTL must still allow a prompt retry of the
|
|
425
|
+
// just-approved operation.
|
|
426
|
+
store.add("allow_once", "hash-default-ttl");
|
|
427
|
+
expect(store.check("allow_once", "hash-default-ttl")).toBe(true);
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("expires after its TTL even if never consumed (ATL-935)", () => {
|
|
431
|
+
// An unconsumed single-use approval must not live forever.
|
|
432
|
+
store.add("allow_once", "hash-once-expire", { durationMs: 1 });
|
|
433
|
+
|
|
434
|
+
const start = Date.now();
|
|
435
|
+
while (Date.now() - start < 5) {
|
|
436
|
+
// spin
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
expect(store.check("allow_once", "hash-once-expire")).toBe(false);
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
// -------------------------------------------------------------------------
|
|
444
|
+
// ATL-935: ephemeral approvals are bounded by TTLs, not by connection
|
|
445
|
+
// teardown.
|
|
446
|
+
//
|
|
447
|
+
// In managed mode the store instance — contents included — is deliberately
|
|
448
|
+
// shared across assistant reconnects and, in the multi-process daemon model,
|
|
449
|
+
// across the connections that each talk to CES, so one guardian approval can
|
|
450
|
+
// be used by any connection entitled to it. The security boundary is the
|
|
451
|
+
// per-grant TTL: an approval that is recorded but never consumed must expire
|
|
452
|
+
// on its own rather than survive indefinitely and be replayed by a much later
|
|
453
|
+
// connection. These tests encode both halves of that contract — an approval
|
|
454
|
+
// stays usable across a reconnect (sharing preserved) but does not outlive
|
|
455
|
+
// its TTL (replay window bounded).
|
|
456
|
+
// -------------------------------------------------------------------------
|
|
457
|
+
describe("ATL-935: ephemeral approvals are bounded by TTLs, not teardown", () => {
|
|
458
|
+
test("every grant kind is bounded by a TTL (no unbounded approvals)", () => {
|
|
459
|
+
// The finding was that allow_once and allow_conversation had no time
|
|
460
|
+
// bound at all. Every kind must now expire on its own.
|
|
461
|
+
store.add("allow_once", "exp-once", { durationMs: 1 });
|
|
462
|
+
store.add("allow_10m", "exp-10m", { durationMs: 1 });
|
|
463
|
+
store.add("allow_conversation", "exp-conv", {
|
|
464
|
+
conversationId: "c",
|
|
465
|
+
durationMs: 1,
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
const start = Date.now();
|
|
469
|
+
while (Date.now() - start < 5) {
|
|
470
|
+
// spin until all three TTLs lapse
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
expect(store.check("allow_once", "exp-once")).toBe(false);
|
|
474
|
+
expect(store.check("allow_10m", "exp-10m")).toBe(false);
|
|
475
|
+
expect(store.check("allow_conversation", "exp-conv", "c")).toBe(false);
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
test("allow_once that is never consumed expires instead of lingering", () => {
|
|
479
|
+
// The headline scenario: an approval recorded but not consumed before a
|
|
480
|
+
// connection drops must not be replayable by a later connection.
|
|
481
|
+
store.add("allow_once", "hash-stale", { durationMs: 1 });
|
|
482
|
+
|
|
483
|
+
const start = Date.now();
|
|
484
|
+
while (Date.now() - start < 5) {
|
|
485
|
+
// spin
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
expect(store.checkAny("hash-stale")).toBeUndefined();
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("allow_conversation is bounded by an absolute TTL backstop", () => {
|
|
492
|
+
store.add("allow_conversation", "hash-conv-ttl", {
|
|
493
|
+
conversationId: "conv-1",
|
|
494
|
+
durationMs: 1,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
const start = Date.now();
|
|
498
|
+
while (Date.now() - start < 5) {
|
|
499
|
+
// spin
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
expect(store.checkAny("hash-conv-ttl", "conv-1")).toBeUndefined();
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
test("an unexpired approval is still shared across a reconnect (no teardown)", () => {
|
|
506
|
+
// The multi-process daemon model depends on this: the store is NOT
|
|
507
|
+
// cleared on disconnect, so an approval granted while one connection was
|
|
508
|
+
// live remains usable by a later or sibling connection within its TTL.
|
|
509
|
+
store.add("allow_conversation", "hash-shared", {
|
|
510
|
+
conversationId: "conv-1",
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
// Simulate a reconnect — no clear() happens between connections.
|
|
514
|
+
expect(store.checkAny("hash-shared", "conv-1")).toBe(
|
|
515
|
+
"allow_conversation",
|
|
516
|
+
);
|
|
517
|
+
// Still usable again (conversation grants are not consumed on use).
|
|
518
|
+
expect(store.checkAny("hash-shared", "conv-1")).toBe(
|
|
519
|
+
"allow_conversation",
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
// An allow_10m approval likewise survives a reconnect within its window.
|
|
523
|
+
store.add("allow_10m", "hash-shared-10m");
|
|
524
|
+
expect(store.checkAny("hash-shared-10m")).toBe("allow_10m");
|
|
525
|
+
});
|
|
422
526
|
});
|
|
423
527
|
|
|
424
528
|
describe("allow_10m", () => {
|
|
@@ -6,11 +6,29 @@
|
|
|
6
6
|
* which is the desired behaviour for ephemeral approvals.
|
|
7
7
|
*
|
|
8
8
|
* Keying:
|
|
9
|
-
* - `allow_once`: Keyed by proposal hash. Consumed (deleted) on first use
|
|
9
|
+
* - `allow_once`: Keyed by proposal hash. Consumed (deleted) on first use, and
|
|
10
|
+
* bounded by a short default TTL so an approval that is never consumed cannot
|
|
11
|
+
* linger and be replayed long after the guardian approved the imminent
|
|
12
|
+
* operation (see ATL-935).
|
|
10
13
|
* - `allow_10m`: Keyed by proposal hash. Checked for expiry on every read;
|
|
11
14
|
* expired entries are lazily purged.
|
|
12
15
|
* - `allow_conversation`: Keyed by proposal hash + conversation ID. Scoped to a
|
|
13
|
-
* single conversation
|
|
16
|
+
* single conversation and bounded by a generous absolute TTL backstop so an
|
|
17
|
+
* approval cannot live for the store's entire (process-long) lifetime and be
|
|
18
|
+
* replayed by a later connection that presents the same conversation ID
|
|
19
|
+
* (see ATL-935).
|
|
20
|
+
*
|
|
21
|
+
* Lifetime note: in managed mode this store instance is process-scoped and
|
|
22
|
+
* deliberately shared across assistant reconnects — and, in the forthcoming
|
|
23
|
+
* multi-process daemon model, across the multiple connections that each talk to
|
|
24
|
+
* CES — so a single guardian approval can be used by any connection entitled to
|
|
25
|
+
* it. Grant lifetime is therefore bounded by per-grant TTLs rather than by
|
|
26
|
+
* connection teardown: every grant kind carries an expiry, so an unconsumed
|
|
27
|
+
* approval expires on its own instead of surviving indefinitely. (A future
|
|
28
|
+
* multi-connection daemon may additionally evict on quiescence — when the count
|
|
29
|
+
* of live CES connections reaches zero — to scope grants to assistant presence;
|
|
30
|
+
* that is connection-lifecycle machinery the multi-connection work should own,
|
|
31
|
+
* and is intentionally not built here.)
|
|
14
32
|
*/
|
|
15
33
|
|
|
16
34
|
// ---------------------------------------------------------------------------
|
|
@@ -28,13 +46,41 @@ export interface TemporaryGrant {
|
|
|
28
46
|
conversationId?: string;
|
|
29
47
|
/** When the grant was created (epoch ms). */
|
|
30
48
|
createdAt: number;
|
|
31
|
-
/** When the grant expires (epoch ms). Set for
|
|
49
|
+
/** When the grant expires (epoch ms). Set for every grant kind: a short
|
|
50
|
+
* default for `allow_once`, the timed window for `allow_10m`, and a generous
|
|
51
|
+
* absolute backstop for `allow_conversation`. */
|
|
32
52
|
expiresAt?: number;
|
|
33
53
|
}
|
|
34
54
|
|
|
35
55
|
/** Default TTL for timed grants (10 minutes). */
|
|
36
56
|
const DEFAULT_TIMED_DURATION_MS = 10 * 60 * 1000;
|
|
37
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Default TTL for single-use (`allow_once`) grants (2 minutes).
|
|
60
|
+
*
|
|
61
|
+
* `allow_once` exists to bridge the gap between a guardian approval and the
|
|
62
|
+
* caller immediately retrying the just-approved operation. Without a TTL, an
|
|
63
|
+
* approval that is never consumed (e.g. the assistant connection drops before
|
|
64
|
+
* the retry) would live for the store's entire lifetime and could later be
|
|
65
|
+
* replayed without a fresh prompt (ATL-935). A short bound keeps the grant
|
|
66
|
+
* usable for a prompt retry while ensuring a stale, unconsumed approval
|
|
67
|
+
* expires on its own.
|
|
68
|
+
*/
|
|
69
|
+
const DEFAULT_ONCE_DURATION_MS = 2 * 60 * 1000;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Absolute TTL backstop for `allow_conversation` grants (12 hours).
|
|
73
|
+
*
|
|
74
|
+
* `allow_conversation` is scoped to a conversation ID and is meant to persist
|
|
75
|
+
* for the life of that conversation, so it is not consumed on use and has no
|
|
76
|
+
* short timeout. But without any bound it would live for the store's entire
|
|
77
|
+
* process-long lifetime and could be replayed by a later connection that
|
|
78
|
+
* presents the same conversation ID long after the original approval (ATL-935).
|
|
79
|
+
* A generous backstop keeps the grant usable across a normal working session
|
|
80
|
+
* while ensuring a long-stale approval eventually requires a fresh prompt.
|
|
81
|
+
*/
|
|
82
|
+
const DEFAULT_CONVERSATION_DURATION_MS = 12 * 60 * 60 * 1000;
|
|
83
|
+
|
|
38
84
|
// ---------------------------------------------------------------------------
|
|
39
85
|
// Store implementation
|
|
40
86
|
// ---------------------------------------------------------------------------
|
|
@@ -102,8 +148,19 @@ export class TemporaryGrantStore {
|
|
|
102
148
|
if (kind === "allow_10m") {
|
|
103
149
|
grant.expiresAt =
|
|
104
150
|
Date.now() + (options?.durationMs ?? DEFAULT_TIMED_DURATION_MS);
|
|
105
|
-
} else if (kind === "allow_once"
|
|
106
|
-
|
|
151
|
+
} else if (kind === "allow_once") {
|
|
152
|
+
// `allow_once` is always bounded by a TTL — a caller-supplied duration
|
|
153
|
+
// when present, otherwise a short default — so an unconsumed single-use
|
|
154
|
+
// approval cannot be replayed indefinitely (ATL-935).
|
|
155
|
+
grant.expiresAt =
|
|
156
|
+
Date.now() + (options?.durationMs ?? DEFAULT_ONCE_DURATION_MS);
|
|
157
|
+
} else if (kind === "allow_conversation") {
|
|
158
|
+
// `allow_conversation` persists for the conversation and is not consumed
|
|
159
|
+
// on use, but it still carries a generous absolute TTL backstop so a
|
|
160
|
+
// conversation-scoped approval cannot linger for the store's entire
|
|
161
|
+
// process lifetime and be replayed by a later connection (ATL-935).
|
|
162
|
+
grant.expiresAt =
|
|
163
|
+
Date.now() + (options?.durationMs ?? DEFAULT_CONVERSATION_DURATION_MS);
|
|
107
164
|
}
|
|
108
165
|
|
|
109
166
|
this.store.set(key, grant);
|
|
@@ -115,8 +172,9 @@ export class TemporaryGrantStore {
|
|
|
115
172
|
* - `allow_once`: Returns `true` and **consumes** the grant (deletes it).
|
|
116
173
|
* - `allow_10m`: Returns `true` only if the grant has not expired.
|
|
117
174
|
* Expired grants are lazily purged.
|
|
118
|
-
* - `allow_conversation`: Returns `true` only if a grant exists
|
|
119
|
-
* proposal hash scoped to the specified conversation ID.
|
|
175
|
+
* - `allow_conversation`: Returns `true` only if a non-expired grant exists
|
|
176
|
+
* for the given proposal hash scoped to the specified conversation ID.
|
|
177
|
+
* Expired grants (past the absolute TTL backstop) are lazily purged.
|
|
120
178
|
*
|
|
121
179
|
* Returns `false` if no matching grant exists.
|
|
122
180
|
*/
|
|
@@ -149,7 +207,12 @@ export class TemporaryGrantStore {
|
|
|
149
207
|
return true;
|
|
150
208
|
}
|
|
151
209
|
|
|
152
|
-
// allow_conversation —
|
|
210
|
+
// allow_conversation — bounded by an absolute TTL backstop; lazily purge an
|
|
211
|
+
// expired grant and deny, mirroring allow_10m.
|
|
212
|
+
if (grant.expiresAt !== undefined && Date.now() >= grant.expiresAt) {
|
|
213
|
+
this.store.delete(key);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
153
216
|
return true;
|
|
154
217
|
}
|
|
155
218
|
|
package/src/managed-main.ts
CHANGED
|
@@ -668,12 +668,23 @@ async function main(): Promise<void> {
|
|
|
668
668
|
// `unregister` miss a tool registered in an earlier session and orphan its
|
|
669
669
|
// bundle.
|
|
670
670
|
//
|
|
671
|
-
// The in-memory temporary-grant store is also process-scoped
|
|
672
|
-
//
|
|
673
|
-
//
|
|
674
|
-
//
|
|
675
|
-
//
|
|
676
|
-
//
|
|
671
|
+
// The in-memory temporary-grant store instance is also process-scoped and is
|
|
672
|
+
// deliberately reused — contents included — across reconnects. Ephemeral
|
|
673
|
+
// approvals (`allow_once` / `allow_10m` / `allow_conversation`) are keyed by
|
|
674
|
+
// proposal hash (plus a caller-supplied conversation ID), not by the
|
|
675
|
+
// connection that produced them, precisely so a single guardian approval can
|
|
676
|
+
// be shared by any connection entitled to use it. That sharing is what the
|
|
677
|
+
// multi-process daemon model needs: several assistant processes will each
|
|
678
|
+
// talk to CES, and an approval granted while one is connected must remain
|
|
679
|
+
// usable by the others. Grant lifetime is therefore bounded by per-grant TTLs
|
|
680
|
+
// (every kind now carries an expiry), not by tearing the store down on
|
|
681
|
+
// disconnect — so an approval that is never consumed expires on its own
|
|
682
|
+
// instead of surviving indefinitely and being replayed by a much later
|
|
683
|
+
// connection without a fresh guardian prompt (ATL-935). A future
|
|
684
|
+
// multi-connection daemon may additionally evict on quiescence (when the
|
|
685
|
+
// count of live CES connections reaches zero) to scope grants to assistant
|
|
686
|
+
// presence; that is connection-lifecycle machinery the multi-connection work
|
|
687
|
+
// should own, and is intentionally not added here.
|
|
677
688
|
//
|
|
678
689
|
// The mutable refs carry the handshake-provided API key and assistant ID;
|
|
679
690
|
// handlers read them at call time. These don't vary across a daemon's
|
|
@@ -682,11 +693,7 @@ async function main(): Promise<void> {
|
|
|
682
693
|
// at call time for audit attribution).
|
|
683
694
|
const apiKeyRef: ApiKeyRef = { current: "" };
|
|
684
695
|
const assistantIdRef: AssistantIdRef = { current: "" };
|
|
685
|
-
const handlers = buildHandlers(
|
|
686
|
-
apiKeyRef,
|
|
687
|
-
assistantIdRef,
|
|
688
|
-
secureKeyBackend,
|
|
689
|
-
);
|
|
696
|
+
const handlers = buildHandlers(apiKeyRef, assistantIdRef, secureKeyBackend);
|
|
690
697
|
|
|
691
698
|
// Serve loop. CES is a long-lived sidecar that must outlive any single
|
|
692
699
|
// assistant session: the assistant container can crash and be restarted
|
|
@@ -782,16 +789,6 @@ async function main(): Promise<void> {
|
|
|
782
789
|
|
|
783
790
|
rpcConnected = false;
|
|
784
791
|
|
|
785
|
-
// Temporary grants are process-shared: they are NOT cleared when a session
|
|
786
|
-
// ends. CES is moving to a model where the assistant daemon's multiple
|
|
787
|
-
// processes each hold their own connection, so an ephemeral approval
|
|
788
|
-
// (`allow_once` / `allow_10m` / `allow_conversation`) granted on one
|
|
789
|
-
// connection must remain usable by the others rather than being scoped to a
|
|
790
|
-
// single session. The store's own semantics keep this safe across a
|
|
791
|
-
// reconnect: `allow_once` is consumed on first use, `allow_10m` is bounded
|
|
792
|
-
// by its wall-clock TTL, and `allow_conversation` is cleared when its
|
|
793
|
-
// conversation ends.
|
|
794
|
-
//
|
|
795
792
|
// A signal-driven end means the process is shutting down; exit the loop.
|
|
796
793
|
// Any other end reason (the assistant disconnected, its stream closed,
|
|
797
794
|
// or the transport errored) means we keep the sidecar up and await a
|