@renseiai/agentfactory-server 0.8.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.
- package/LICENSE +21 -0
- package/README.md +71 -0
- package/dist/src/a2a-server.d.ts +88 -0
- package/dist/src/a2a-server.d.ts.map +1 -0
- package/dist/src/a2a-server.integration.test.d.ts +9 -0
- package/dist/src/a2a-server.integration.test.d.ts.map +1 -0
- package/dist/src/a2a-server.integration.test.js +397 -0
- package/dist/src/a2a-server.js +235 -0
- package/dist/src/a2a-server.test.d.ts +2 -0
- package/dist/src/a2a-server.test.d.ts.map +1 -0
- package/dist/src/a2a-server.test.js +311 -0
- package/dist/src/a2a-types.d.ts +125 -0
- package/dist/src/a2a-types.d.ts.map +1 -0
- package/dist/src/a2a-types.js +8 -0
- package/dist/src/agent-tracking.d.ts +201 -0
- package/dist/src/agent-tracking.d.ts.map +1 -0
- package/dist/src/agent-tracking.js +349 -0
- package/dist/src/env-validation.d.ts +65 -0
- package/dist/src/env-validation.d.ts.map +1 -0
- package/dist/src/env-validation.js +134 -0
- package/dist/src/governor-dedup.d.ts +15 -0
- package/dist/src/governor-dedup.d.ts.map +1 -0
- package/dist/src/governor-dedup.js +31 -0
- package/dist/src/governor-event-bus.d.ts +54 -0
- package/dist/src/governor-event-bus.d.ts.map +1 -0
- package/dist/src/governor-event-bus.js +152 -0
- package/dist/src/governor-storage.d.ts +28 -0
- package/dist/src/governor-storage.d.ts.map +1 -0
- package/dist/src/governor-storage.js +52 -0
- package/dist/src/index.d.ts +26 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +50 -0
- package/dist/src/issue-lock.d.ts +129 -0
- package/dist/src/issue-lock.d.ts.map +1 -0
- package/dist/src/issue-lock.js +508 -0
- package/dist/src/logger.d.ts +76 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +218 -0
- package/dist/src/orphan-cleanup.d.ts +64 -0
- package/dist/src/orphan-cleanup.d.ts.map +1 -0
- package/dist/src/orphan-cleanup.js +369 -0
- package/dist/src/pending-prompts.d.ts +67 -0
- package/dist/src/pending-prompts.d.ts.map +1 -0
- package/dist/src/pending-prompts.js +176 -0
- package/dist/src/processing-state-storage.d.ts +38 -0
- package/dist/src/processing-state-storage.d.ts.map +1 -0
- package/dist/src/processing-state-storage.js +61 -0
- package/dist/src/quota-tracker.d.ts +62 -0
- package/dist/src/quota-tracker.d.ts.map +1 -0
- package/dist/src/quota-tracker.js +155 -0
- package/dist/src/rate-limit.d.ts +111 -0
- package/dist/src/rate-limit.d.ts.map +1 -0
- package/dist/src/rate-limit.js +171 -0
- package/dist/src/redis-circuit-breaker.d.ts +67 -0
- package/dist/src/redis-circuit-breaker.d.ts.map +1 -0
- package/dist/src/redis-circuit-breaker.js +290 -0
- package/dist/src/redis-rate-limiter.d.ts +51 -0
- package/dist/src/redis-rate-limiter.d.ts.map +1 -0
- package/dist/src/redis-rate-limiter.js +168 -0
- package/dist/src/redis.d.ts +146 -0
- package/dist/src/redis.d.ts.map +1 -0
- package/dist/src/redis.js +343 -0
- package/dist/src/session-hash.d.ts +48 -0
- package/dist/src/session-hash.d.ts.map +1 -0
- package/dist/src/session-hash.js +80 -0
- package/dist/src/session-storage.d.ts +166 -0
- package/dist/src/session-storage.d.ts.map +1 -0
- package/dist/src/session-storage.js +397 -0
- package/dist/src/token-storage.d.ts +118 -0
- package/dist/src/token-storage.d.ts.map +1 -0
- package/dist/src/token-storage.js +263 -0
- package/dist/src/types.d.ts +11 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +7 -0
- package/dist/src/webhook-idempotency.d.ts +44 -0
- package/dist/src/webhook-idempotency.d.ts.map +1 -0
- package/dist/src/webhook-idempotency.js +148 -0
- package/dist/src/work-queue.d.ts +120 -0
- package/dist/src/work-queue.d.ts.map +1 -0
- package/dist/src/work-queue.js +384 -0
- package/dist/src/worker-auth.d.ts +29 -0
- package/dist/src/worker-auth.d.ts.map +1 -0
- package/dist/src/worker-auth.js +49 -0
- package/dist/src/worker-storage.d.ts +108 -0
- package/dist/src/worker-storage.d.ts.map +1 -0
- package/dist/src/worker-storage.js +295 -0
- package/dist/src/workflow-state-integration.test.d.ts +2 -0
- package/dist/src/workflow-state-integration.test.d.ts.map +1 -0
- package/dist/src/workflow-state-integration.test.js +342 -0
- package/dist/src/workflow-state.test.d.ts +2 -0
- package/dist/src/workflow-state.test.d.ts.map +1 -0
- package/dist/src/workflow-state.test.js +113 -0
- package/package.json +72 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Variable Validation
|
|
3
|
+
*
|
|
4
|
+
* Validates required environment variables on startup.
|
|
5
|
+
* Fails fast if critical security variables are missing in production.
|
|
6
|
+
*/
|
|
7
|
+
import { createLogger } from './logger.js';
|
|
8
|
+
const log = createLogger('env-validation');
|
|
9
|
+
/**
|
|
10
|
+
* Default required environment variables for production
|
|
11
|
+
* These are critical for security and must be present
|
|
12
|
+
*/
|
|
13
|
+
const DEFAULT_REQUIRED_VARS = [
|
|
14
|
+
'LINEAR_WEBHOOK_SECRET',
|
|
15
|
+
'CRON_SECRET',
|
|
16
|
+
'WORKER_API_KEY',
|
|
17
|
+
'SESSION_HASH_SALT',
|
|
18
|
+
];
|
|
19
|
+
/**
|
|
20
|
+
* Default minimum length validations
|
|
21
|
+
*/
|
|
22
|
+
const DEFAULT_MIN_LENGTH_VARS = [
|
|
23
|
+
{ name: 'SESSION_HASH_SALT', minLength: 32 },
|
|
24
|
+
];
|
|
25
|
+
/**
|
|
26
|
+
* Check if running in production environment
|
|
27
|
+
*/
|
|
28
|
+
function isProduction() {
|
|
29
|
+
return (process.env.NODE_ENV === 'production' ||
|
|
30
|
+
process.env.VERCEL_ENV === 'production');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validate environment variables
|
|
34
|
+
*
|
|
35
|
+
* In production: All required vars must be present
|
|
36
|
+
* In development: Log warnings for missing vars but don't fail
|
|
37
|
+
*
|
|
38
|
+
* @param config - Optional configuration to override defaults
|
|
39
|
+
* @returns Validation result with missing vars
|
|
40
|
+
*/
|
|
41
|
+
export function validateEnv(config) {
|
|
42
|
+
const missing = [];
|
|
43
|
+
const warnings = [];
|
|
44
|
+
const requiredVars = config?.requiredVars ?? DEFAULT_REQUIRED_VARS;
|
|
45
|
+
const minLengthVars = config?.minLengthVars ?? DEFAULT_MIN_LENGTH_VARS;
|
|
46
|
+
for (const varName of requiredVars) {
|
|
47
|
+
if (!process.env[varName]) {
|
|
48
|
+
if (isProduction()) {
|
|
49
|
+
missing.push(varName);
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
warnings.push(varName);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
// Additional validation for minimum length variables
|
|
57
|
+
for (const { name, minLength } of minLengthVars) {
|
|
58
|
+
const value = process.env[name];
|
|
59
|
+
if (value && value.length < minLength) {
|
|
60
|
+
const msg = `${name} should be at least ${minLength} characters for security`;
|
|
61
|
+
if (isProduction()) {
|
|
62
|
+
missing.push(`${name} (too short)`);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
warnings.push(msg);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
valid: missing.length === 0,
|
|
71
|
+
missing,
|
|
72
|
+
warnings,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Validate and fail fast if critical vars are missing
|
|
77
|
+
*
|
|
78
|
+
* Call this at application startup to ensure required
|
|
79
|
+
* environment variables are configured.
|
|
80
|
+
*
|
|
81
|
+
* @param config - Optional configuration to override defaults
|
|
82
|
+
* @throws Error if required vars are missing in production
|
|
83
|
+
*/
|
|
84
|
+
export function validateEnvOrThrow(config) {
|
|
85
|
+
const result = validateEnv(config);
|
|
86
|
+
// Log warnings for development
|
|
87
|
+
if (result.warnings.length > 0) {
|
|
88
|
+
log.warn('Missing environment variables (development mode)', {
|
|
89
|
+
variables: result.warnings,
|
|
90
|
+
hint: 'These are required in production',
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// Fail in production if required vars are missing
|
|
94
|
+
if (!result.valid) {
|
|
95
|
+
const errorMsg = `Missing required environment variables: ${result.missing.join(', ')}`;
|
|
96
|
+
log.error(errorMsg, {
|
|
97
|
+
missing: result.missing,
|
|
98
|
+
environment: process.env.NODE_ENV,
|
|
99
|
+
});
|
|
100
|
+
throw new Error(errorMsg);
|
|
101
|
+
}
|
|
102
|
+
log.debug('Environment validation passed');
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Check if webhook signature verification is configured
|
|
106
|
+
*/
|
|
107
|
+
export function isWebhookSecretConfigured() {
|
|
108
|
+
return !!process.env.LINEAR_WEBHOOK_SECRET;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Check if cron authentication is configured
|
|
112
|
+
*/
|
|
113
|
+
export function isCronSecretConfigured() {
|
|
114
|
+
return !!process.env.CRON_SECRET;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Check if session hashing is configured
|
|
118
|
+
*/
|
|
119
|
+
export function isSessionHashConfigured() {
|
|
120
|
+
const salt = process.env.SESSION_HASH_SALT;
|
|
121
|
+
return !!salt && salt.length >= 32;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Get session hash salt
|
|
125
|
+
* @param saltEnvVar - Environment variable name for the salt (default: SESSION_HASH_SALT)
|
|
126
|
+
* @throws Error if not configured
|
|
127
|
+
*/
|
|
128
|
+
export function getSessionHashSalt(saltEnvVar = 'SESSION_HASH_SALT') {
|
|
129
|
+
const salt = process.env[saltEnvVar];
|
|
130
|
+
if (!salt) {
|
|
131
|
+
throw new Error(`${saltEnvVar} not configured`);
|
|
132
|
+
}
|
|
133
|
+
return salt;
|
|
134
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis Event Deduplicator
|
|
3
|
+
*
|
|
4
|
+
* Production EventDeduplicator backed by Redis SETNX with TTL.
|
|
5
|
+
* Key pattern: `governor:dedup:{dedupKey}`
|
|
6
|
+
*/
|
|
7
|
+
import type { EventDeduplicator, EventDeduplicatorConfig } from '@renseiai/agentfactory';
|
|
8
|
+
export declare class RedisEventDeduplicator implements EventDeduplicator {
|
|
9
|
+
private readonly windowMs;
|
|
10
|
+
private readonly keyPrefix;
|
|
11
|
+
constructor(config?: Partial<EventDeduplicatorConfig>, keyPrefix?: string);
|
|
12
|
+
isDuplicate(key: string): Promise<boolean>;
|
|
13
|
+
clear(): Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=governor-dedup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"governor-dedup.d.ts","sourceRoot":"","sources":["../../src/governor-dedup.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,uBAAuB,EAAE,MAAM,wBAAwB,CAAA;AAQxF,qBAAa,sBAAuB,YAAW,iBAAiB;IAC9D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAQ;IACjC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;gBAGhC,MAAM,GAAE,OAAO,CAAC,uBAAuB,CAAM,EAC7C,SAAS,SAAmB;IAMxB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAS1C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAK7B"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis Event Deduplicator
|
|
3
|
+
*
|
|
4
|
+
* Production EventDeduplicator backed by Redis SETNX with TTL.
|
|
5
|
+
* Key pattern: `governor:dedup:{dedupKey}`
|
|
6
|
+
*/
|
|
7
|
+
import { DEFAULT_DEDUP_CONFIG } from '@renseiai/agentfactory';
|
|
8
|
+
import { redisSetNX } from './redis.js';
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// RedisEventDeduplicator
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
export class RedisEventDeduplicator {
|
|
13
|
+
windowMs;
|
|
14
|
+
keyPrefix;
|
|
15
|
+
constructor(config = {}, keyPrefix = 'governor:dedup') {
|
|
16
|
+
this.windowMs = config.windowMs ?? DEFAULT_DEDUP_CONFIG.windowMs;
|
|
17
|
+
this.keyPrefix = keyPrefix;
|
|
18
|
+
}
|
|
19
|
+
async isDuplicate(key) {
|
|
20
|
+
const redisKey = `${this.keyPrefix}:${key}`;
|
|
21
|
+
const ttlSeconds = Math.max(1, Math.ceil(this.windowMs / 1000));
|
|
22
|
+
// SETNX returns true if key was newly set (not a duplicate)
|
|
23
|
+
const wasSet = await redisSetNX(redisKey, '1', ttlSeconds);
|
|
24
|
+
return !wasSet; // if we couldn't set, it's a duplicate
|
|
25
|
+
}
|
|
26
|
+
async clear() {
|
|
27
|
+
// In production, keys auto-expire via TTL.
|
|
28
|
+
// Explicit clear is mainly for testing — not implemented for Redis
|
|
29
|
+
// since test suites should use InMemoryEventDeduplicator.
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis Event Bus
|
|
3
|
+
*
|
|
4
|
+
* Production GovernorEventBus backed by Redis Streams.
|
|
5
|
+
* Uses a consumer group for reliable delivery and horizontal scaling.
|
|
6
|
+
*
|
|
7
|
+
* Stream: `governor:events`
|
|
8
|
+
* Consumer group: `governor-group`
|
|
9
|
+
* MAXLEN: 10,000 (approximate trim)
|
|
10
|
+
*/
|
|
11
|
+
import type { GovernorEvent, GovernorEventBus } from '@renseiai/agentfactory';
|
|
12
|
+
export interface RedisEventBusConfig {
|
|
13
|
+
/** Redis stream key (default: 'governor:events') */
|
|
14
|
+
streamKey?: string;
|
|
15
|
+
/** Consumer group name (default: 'governor-group') */
|
|
16
|
+
groupName?: string;
|
|
17
|
+
/** Consumer name within the group (default: hostname or random) */
|
|
18
|
+
consumerName?: string;
|
|
19
|
+
/** Max stream length — approximate trim (default: 10000) */
|
|
20
|
+
maxLen?: number;
|
|
21
|
+
/** Block timeout in milliseconds for XREADGROUP (default: 5000) */
|
|
22
|
+
blockMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export declare class RedisEventBus implements GovernorEventBus {
|
|
25
|
+
private readonly streamKey;
|
|
26
|
+
private readonly groupName;
|
|
27
|
+
private readonly consumerName;
|
|
28
|
+
private readonly maxLen;
|
|
29
|
+
private readonly blockMs;
|
|
30
|
+
private closed;
|
|
31
|
+
private groupCreated;
|
|
32
|
+
constructor(config?: RedisEventBusConfig);
|
|
33
|
+
/**
|
|
34
|
+
* Ensure the consumer group exists. Idempotent — safe to call multiple times.
|
|
35
|
+
*/
|
|
36
|
+
private ensureGroup;
|
|
37
|
+
publish(event: GovernorEvent): Promise<string>;
|
|
38
|
+
subscribe(): AsyncGenerator<{
|
|
39
|
+
id: string;
|
|
40
|
+
event: GovernorEvent;
|
|
41
|
+
}>;
|
|
42
|
+
ack(eventId: string): Promise<void>;
|
|
43
|
+
close(): Promise<void>;
|
|
44
|
+
/**
|
|
45
|
+
* Re-deliver pending messages (claimed but not acked from a previous run).
|
|
46
|
+
*/
|
|
47
|
+
private readPending;
|
|
48
|
+
/**
|
|
49
|
+
* Parse a Redis stream message fields array into a GovernorEvent.
|
|
50
|
+
* Fields come as [key1, val1, key2, val2, ...].
|
|
51
|
+
*/
|
|
52
|
+
private parseEvent;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=governor-event-bus.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"governor-event-bus.d.ts","sourceRoot":"","sources":["../../src/governor-event-bus.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAO7E,MAAM,WAAW,mBAAmB;IAClC,oDAAoD;IACpD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,sDAAsD;IACtD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAA;CACjB;AAWD,qBAAa,aAAc,YAAW,gBAAgB;IACpD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAQ;IACrC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAQ;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAQ;IAChC,OAAO,CAAC,MAAM,CAAQ;IACtB,OAAO,CAAC,YAAY,CAAQ;gBAEhB,MAAM,GAAE,mBAAwB;IAQ5C;;OAEG;YACW,WAAW;IAenB,OAAO,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,MAAM,CAAC;IA0B7C,SAAS,IAAI,cAAc,CAAC;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,aAAa,CAAA;KAAE,CAAC;IAiDlE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAKnC,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B;;OAEG;YACY,WAAW;IAiC1B;;;OAGG;IACH,OAAO,CAAC,UAAU;CAanB"}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis Event Bus
|
|
3
|
+
*
|
|
4
|
+
* Production GovernorEventBus backed by Redis Streams.
|
|
5
|
+
* Uses a consumer group for reliable delivery and horizontal scaling.
|
|
6
|
+
*
|
|
7
|
+
* Stream: `governor:events`
|
|
8
|
+
* Consumer group: `governor-group`
|
|
9
|
+
* MAXLEN: 10,000 (approximate trim)
|
|
10
|
+
*/
|
|
11
|
+
import { getRedisClient } from './redis.js';
|
|
12
|
+
const DEFAULT_STREAM_KEY = 'governor:events';
|
|
13
|
+
const DEFAULT_GROUP_NAME = 'governor-group';
|
|
14
|
+
const DEFAULT_MAX_LEN = 10_000;
|
|
15
|
+
const DEFAULT_BLOCK_MS = 5_000;
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// RedisEventBus
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
export class RedisEventBus {
|
|
20
|
+
streamKey;
|
|
21
|
+
groupName;
|
|
22
|
+
consumerName;
|
|
23
|
+
maxLen;
|
|
24
|
+
blockMs;
|
|
25
|
+
closed = false;
|
|
26
|
+
groupCreated = false;
|
|
27
|
+
constructor(config = {}) {
|
|
28
|
+
this.streamKey = config.streamKey ?? DEFAULT_STREAM_KEY;
|
|
29
|
+
this.groupName = config.groupName ?? DEFAULT_GROUP_NAME;
|
|
30
|
+
this.consumerName = config.consumerName ?? `governor-${process.pid}-${Date.now()}`;
|
|
31
|
+
this.maxLen = config.maxLen ?? DEFAULT_MAX_LEN;
|
|
32
|
+
this.blockMs = config.blockMs ?? DEFAULT_BLOCK_MS;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Ensure the consumer group exists. Idempotent — safe to call multiple times.
|
|
36
|
+
*/
|
|
37
|
+
async ensureGroup() {
|
|
38
|
+
if (this.groupCreated)
|
|
39
|
+
return;
|
|
40
|
+
const redis = getRedisClient();
|
|
41
|
+
try {
|
|
42
|
+
await redis.xgroup('CREATE', this.streamKey, this.groupName, '0', 'MKSTREAM');
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
// BUSYGROUP = group already exists, which is fine
|
|
46
|
+
if (err instanceof Error && !err.message.includes('BUSYGROUP')) {
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
this.groupCreated = true;
|
|
51
|
+
}
|
|
52
|
+
async publish(event) {
|
|
53
|
+
if (this.closed) {
|
|
54
|
+
throw new Error('Event bus is closed');
|
|
55
|
+
}
|
|
56
|
+
const redis = getRedisClient();
|
|
57
|
+
const payload = JSON.stringify(event);
|
|
58
|
+
// XADD with approximate MAXLEN trim
|
|
59
|
+
const id = await redis.xadd(this.streamKey, 'MAXLEN', '~', String(this.maxLen), '*', 'data', payload);
|
|
60
|
+
if (!id) {
|
|
61
|
+
throw new Error('XADD returned null — stream write failed');
|
|
62
|
+
}
|
|
63
|
+
return id;
|
|
64
|
+
}
|
|
65
|
+
async *subscribe() {
|
|
66
|
+
await this.ensureGroup();
|
|
67
|
+
const redis = getRedisClient();
|
|
68
|
+
// First, re-deliver any pending messages (from previous crash)
|
|
69
|
+
yield* this.readPending(redis);
|
|
70
|
+
// Then read new messages
|
|
71
|
+
while (!this.closed) {
|
|
72
|
+
try {
|
|
73
|
+
const results = await redis.xreadgroup('GROUP', this.groupName, this.consumerName, 'COUNT', '1', 'BLOCK', this.blockMs, 'STREAMS', this.streamKey, '>');
|
|
74
|
+
if (!results || results.length === 0) {
|
|
75
|
+
continue; // timeout, loop back
|
|
76
|
+
}
|
|
77
|
+
for (const [, messages] of results) {
|
|
78
|
+
for (const [id, fields] of messages) {
|
|
79
|
+
const event = this.parseEvent(fields);
|
|
80
|
+
if (event) {
|
|
81
|
+
yield { id, event };
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
// Malformed event — ack and skip
|
|
85
|
+
await this.ack(id);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch (err) {
|
|
91
|
+
if (this.closed)
|
|
92
|
+
return;
|
|
93
|
+
// Log and continue on transient errors
|
|
94
|
+
console.error('[redis-event-bus] Error reading from stream:', err);
|
|
95
|
+
// Brief backoff before retry
|
|
96
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
async ack(eventId) {
|
|
101
|
+
const redis = getRedisClient();
|
|
102
|
+
await redis.xack(this.streamKey, this.groupName, eventId);
|
|
103
|
+
}
|
|
104
|
+
async close() {
|
|
105
|
+
this.closed = true;
|
|
106
|
+
}
|
|
107
|
+
// ---- Internal ----
|
|
108
|
+
/**
|
|
109
|
+
* Re-deliver pending messages (claimed but not acked from a previous run).
|
|
110
|
+
*/
|
|
111
|
+
async *readPending(redis) {
|
|
112
|
+
try {
|
|
113
|
+
const results = await redis.xreadgroup('GROUP', this.groupName, this.consumerName, 'COUNT', '100', 'STREAMS', this.streamKey, '0');
|
|
114
|
+
if (!results || results.length === 0)
|
|
115
|
+
return;
|
|
116
|
+
for (const [, messages] of results) {
|
|
117
|
+
for (const [id, fields] of messages) {
|
|
118
|
+
if (!fields || fields.length === 0)
|
|
119
|
+
continue; // already acked
|
|
120
|
+
const event = this.parseEvent(fields);
|
|
121
|
+
if (event) {
|
|
122
|
+
yield { id, event };
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
await this.ack(id);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
console.error('[redis-event-bus] Error reading pending messages:', err);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Parse a Redis stream message fields array into a GovernorEvent.
|
|
136
|
+
* Fields come as [key1, val1, key2, val2, ...].
|
|
137
|
+
*/
|
|
138
|
+
parseEvent(fields) {
|
|
139
|
+
try {
|
|
140
|
+
// Find the 'data' field
|
|
141
|
+
for (let i = 0; i < fields.length; i += 2) {
|
|
142
|
+
if (fields[i] === 'data') {
|
|
143
|
+
return JSON.parse(fields[i + 1]);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governor Storage — Redis Implementation
|
|
3
|
+
*
|
|
4
|
+
* Redis-backed storage adapter for the human touchpoint override state.
|
|
5
|
+
* Implements the OverrideStorage interface from @renseiai/agentfactory (packages/core).
|
|
6
|
+
*/
|
|
7
|
+
import type { OverrideStorage, OverrideState } from '@renseiai/agentfactory';
|
|
8
|
+
/**
|
|
9
|
+
* Redis-backed override storage for production use.
|
|
10
|
+
*
|
|
11
|
+
* Stores override state under `governor:override:{issueId}` keys
|
|
12
|
+
* with a 30-day TTL.
|
|
13
|
+
*/
|
|
14
|
+
export declare class RedisOverrideStorage implements OverrideStorage {
|
|
15
|
+
/**
|
|
16
|
+
* Get the override state for an issue from Redis
|
|
17
|
+
*/
|
|
18
|
+
get(issueId: string): Promise<OverrideState | null>;
|
|
19
|
+
/**
|
|
20
|
+
* Persist override state for an issue to Redis
|
|
21
|
+
*/
|
|
22
|
+
set(issueId: string, state: OverrideState): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Remove override state for an issue from Redis
|
|
25
|
+
*/
|
|
26
|
+
clear(issueId: string): Promise<void>;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=governor-storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"governor-storage.d.ts","sourceRoot":"","sources":["../../src/governor-storage.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,wBAAwB,CAAA;AAgB5E;;;;;GAKG;AACH,qBAAa,oBAAqB,YAAW,eAAe;IAC1D;;OAEG;IACG,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC;IAWzD;;OAEG;IACG,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAM/D;;OAEG;IACG,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAK5C"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Governor Storage — Redis Implementation
|
|
3
|
+
*
|
|
4
|
+
* Redis-backed storage adapter for the human touchpoint override state.
|
|
5
|
+
* Implements the OverrideStorage interface from @renseiai/agentfactory (packages/core).
|
|
6
|
+
*/
|
|
7
|
+
import { redisSet, redisGet, redisDel } from './redis.js';
|
|
8
|
+
const log = {
|
|
9
|
+
info: (msg, data) => console.log(`[governor-storage] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
10
|
+
warn: (msg, data) => console.warn(`[governor-storage] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
11
|
+
error: (msg, data) => console.error(`[governor-storage] ${msg}`, data ? JSON.stringify(data) : ''),
|
|
12
|
+
debug: (_msg, _data) => { },
|
|
13
|
+
};
|
|
14
|
+
/** Redis key prefix for governor override state */
|
|
15
|
+
const GOVERNOR_OVERRIDE_PREFIX = 'governor:override:';
|
|
16
|
+
/** TTL for override state: 30 days in seconds */
|
|
17
|
+
const GOVERNOR_OVERRIDE_TTL = 30 * 24 * 60 * 60;
|
|
18
|
+
/**
|
|
19
|
+
* Redis-backed override storage for production use.
|
|
20
|
+
*
|
|
21
|
+
* Stores override state under `governor:override:{issueId}` keys
|
|
22
|
+
* with a 30-day TTL.
|
|
23
|
+
*/
|
|
24
|
+
export class RedisOverrideStorage {
|
|
25
|
+
/**
|
|
26
|
+
* Get the override state for an issue from Redis
|
|
27
|
+
*/
|
|
28
|
+
async get(issueId) {
|
|
29
|
+
const key = `${GOVERNOR_OVERRIDE_PREFIX}${issueId}`;
|
|
30
|
+
const state = await redisGet(key);
|
|
31
|
+
if (state) {
|
|
32
|
+
log.debug('Retrieved override state', { issueId, type: state.directive.type });
|
|
33
|
+
}
|
|
34
|
+
return state;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Persist override state for an issue to Redis
|
|
38
|
+
*/
|
|
39
|
+
async set(issueId, state) {
|
|
40
|
+
const key = `${GOVERNOR_OVERRIDE_PREFIX}${issueId}`;
|
|
41
|
+
await redisSet(key, state, GOVERNOR_OVERRIDE_TTL);
|
|
42
|
+
log.info('Stored override state', { issueId, type: state.directive.type });
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Remove override state for an issue from Redis
|
|
46
|
+
*/
|
|
47
|
+
async clear(issueId) {
|
|
48
|
+
const key = `${GOVERNOR_OVERRIDE_PREFIX}${issueId}`;
|
|
49
|
+
await redisDel(key);
|
|
50
|
+
log.info('Cleared override state', { issueId });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export * from './logger.js';
|
|
2
|
+
export * from './types.js';
|
|
3
|
+
export * from './redis.js';
|
|
4
|
+
export * from './session-storage.js';
|
|
5
|
+
export * from './work-queue.js';
|
|
6
|
+
export * from './worker-storage.js';
|
|
7
|
+
export * from './issue-lock.js';
|
|
8
|
+
export * from './agent-tracking.js';
|
|
9
|
+
export * from './webhook-idempotency.js';
|
|
10
|
+
export * from './pending-prompts.js';
|
|
11
|
+
export * from './orphan-cleanup.js';
|
|
12
|
+
export * from './worker-auth.js';
|
|
13
|
+
export * from './session-hash.js';
|
|
14
|
+
export * from './rate-limit.js';
|
|
15
|
+
export * from './token-storage.js';
|
|
16
|
+
export * from './env-validation.js';
|
|
17
|
+
export * from './governor-storage.js';
|
|
18
|
+
export * from './processing-state-storage.js';
|
|
19
|
+
export * from './governor-event-bus.js';
|
|
20
|
+
export * from './governor-dedup.js';
|
|
21
|
+
export * from './redis-rate-limiter.js';
|
|
22
|
+
export * from './redis-circuit-breaker.js';
|
|
23
|
+
export * from './quota-tracker.js';
|
|
24
|
+
export * from './a2a-types.js';
|
|
25
|
+
export * from './a2a-server.js';
|
|
26
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AACA,cAAc,aAAa,CAAA;AAG3B,cAAc,YAAY,CAAA;AAG1B,cAAc,YAAY,CAAA;AAG1B,cAAc,sBAAsB,CAAA;AAGpC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,qBAAqB,CAAA;AAGnC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,qBAAqB,CAAA;AAGnC,cAAc,0BAA0B,CAAA;AAGxC,cAAc,sBAAsB,CAAA;AAGpC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,kBAAkB,CAAA;AAGhC,cAAc,mBAAmB,CAAA;AAGjC,cAAc,iBAAiB,CAAA;AAG/B,cAAc,oBAAoB,CAAA;AAGlC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,uBAAuB,CAAA;AAGrC,cAAc,+BAA+B,CAAA;AAG7C,cAAc,yBAAyB,CAAA;AAGvC,cAAc,qBAAqB,CAAA;AAGnC,cAAc,yBAAyB,CAAA;AAGvC,cAAc,4BAA4B,CAAA;AAG1C,cAAc,oBAAoB,CAAA;AAGlC,cAAc,gBAAgB,CAAA;AAG9B,cAAc,iBAAiB,CAAA"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Logger
|
|
2
|
+
export * from './logger.js';
|
|
3
|
+
// Types
|
|
4
|
+
export * from './types.js';
|
|
5
|
+
// Redis client
|
|
6
|
+
export * from './redis.js';
|
|
7
|
+
// Session management
|
|
8
|
+
export * from './session-storage.js';
|
|
9
|
+
// Work queue
|
|
10
|
+
export * from './work-queue.js';
|
|
11
|
+
// Worker pool
|
|
12
|
+
export * from './worker-storage.js';
|
|
13
|
+
// Issue locking
|
|
14
|
+
export * from './issue-lock.js';
|
|
15
|
+
// Agent tracking
|
|
16
|
+
export * from './agent-tracking.js';
|
|
17
|
+
// Webhook idempotency
|
|
18
|
+
export * from './webhook-idempotency.js';
|
|
19
|
+
// Pending prompts
|
|
20
|
+
export * from './pending-prompts.js';
|
|
21
|
+
// Orphan cleanup
|
|
22
|
+
export * from './orphan-cleanup.js';
|
|
23
|
+
// Worker authentication
|
|
24
|
+
export * from './worker-auth.js';
|
|
25
|
+
// Session hashing
|
|
26
|
+
export * from './session-hash.js';
|
|
27
|
+
// Rate limiting
|
|
28
|
+
export * from './rate-limit.js';
|
|
29
|
+
// Token storage
|
|
30
|
+
export * from './token-storage.js';
|
|
31
|
+
// Environment validation
|
|
32
|
+
export * from './env-validation.js';
|
|
33
|
+
// Governor storage (Redis-backed override state)
|
|
34
|
+
export * from './governor-storage.js';
|
|
35
|
+
// Processing state storage (Redis-backed top-of-funnel phase tracking)
|
|
36
|
+
export * from './processing-state-storage.js';
|
|
37
|
+
// Governor event bus (Redis Streams)
|
|
38
|
+
export * from './governor-event-bus.js';
|
|
39
|
+
// Governor event deduplicator (Redis SETNX)
|
|
40
|
+
export * from './governor-dedup.js';
|
|
41
|
+
// Redis-backed Linear API rate limiter (shared across processes)
|
|
42
|
+
export * from './redis-rate-limiter.js';
|
|
43
|
+
// Redis-backed circuit breaker (shared across processes)
|
|
44
|
+
export * from './redis-circuit-breaker.js';
|
|
45
|
+
// Linear API quota tracker (reads rate limit headers)
|
|
46
|
+
export * from './quota-tracker.js';
|
|
47
|
+
// A2A Protocol types
|
|
48
|
+
export * from './a2a-types.js';
|
|
49
|
+
// A2A Server (AgentCard + JSON-RPC handlers)
|
|
50
|
+
export * from './a2a-server.js';
|