agent-relay-server 0.15.1 → 0.17.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/docs/openapi.json +101 -1
- package/package.json +2 -2
- package/public/index.html +75 -3
- package/src/automations.ts +19 -18
- package/src/bus-outbox.ts +5 -5
- package/src/bus.ts +1 -1
- package/src/commands-db.ts +5 -5
- package/src/config-store.ts +71 -12
- package/src/db.ts +311 -229
- package/src/insights-db.ts +6 -6
- package/src/lifecycle-manager.ts +3 -3
- package/src/maintenance.ts +25 -6
- package/src/managed-policy.ts +2 -1
- package/src/memory-sqlite-broker.ts +12 -12
- package/src/provider-catalog-store.ts +2 -2
- package/src/recipe-db.ts +9 -9
- package/src/routes.ts +44 -0
- package/src/security.ts +1 -1
- package/src/token-db.ts +10 -10
package/src/config-store.ts
CHANGED
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
SpawnPolicy,
|
|
13
13
|
SpawnProvider,
|
|
14
14
|
StewardConfig,
|
|
15
|
+
WorkspaceConfig,
|
|
15
16
|
} from "./types";
|
|
16
17
|
|
|
17
18
|
const CONFIG_HISTORY_LIMIT = 50;
|
|
@@ -21,6 +22,8 @@ const STEWARD_NAMESPACE = "steward";
|
|
|
21
22
|
const STEWARD_KEY = "default";
|
|
22
23
|
const INSIGHTS_NAMESPACE = "insights";
|
|
23
24
|
const INSIGHTS_KEY = "default";
|
|
25
|
+
const WORKSPACE_NAMESPACE = "workspace";
|
|
26
|
+
const WORKSPACE_KEY = "default";
|
|
24
27
|
const VALID_PROVIDERS = ["claude", "codex"] as const;
|
|
25
28
|
const VALID_PROFILE_PROVIDERS = ["any", "claude", "codex"] as const;
|
|
26
29
|
const VALID_PROFILE_BASES = ["host", "minimal", "isolated"] as const;
|
|
@@ -472,24 +475,46 @@ function validateInsightsConfig(value: unknown): InsightsConfig {
|
|
|
472
475
|
};
|
|
473
476
|
}
|
|
474
477
|
|
|
478
|
+
// Global workspace provisioning config for isolated worktrees (#159 follow-up).
|
|
479
|
+
// Defaults seed the two untracked paths an isolated agent almost always needs:
|
|
480
|
+
// the agent guide and the rig config, both gitignored so a fresh worktree lacks them.
|
|
481
|
+
const WORKSPACE_CONFIG_DEFAULTS: WorkspaceConfig = {
|
|
482
|
+
symlinkPaths: ["AGENTS.md", ".claude-rig"],
|
|
483
|
+
};
|
|
484
|
+
|
|
485
|
+
function validateWorkspaceConfig(value: unknown): WorkspaceConfig {
|
|
486
|
+
if (!isRecord(value)) throw new ValidationError("workspace config value must be an object");
|
|
487
|
+
const symlinkPaths = cleanStringArray(value.symlinkPaths, "symlinkPaths");
|
|
488
|
+
// Reject absolute paths and parent-traversal up front: symlink sources must stay
|
|
489
|
+
// inside the main checkout. The orchestrator re-checks containment at link time,
|
|
490
|
+
// but failing here gives the operator immediate feedback in the dashboard.
|
|
491
|
+
for (const entry of symlinkPaths) {
|
|
492
|
+
if (entry.startsWith("/") || entry.split(/[\\/]/).includes("..")) {
|
|
493
|
+
throw new ValidationError(`symlinkPaths entry must be a relative path within the repo: ${entry}`);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return { symlinkPaths };
|
|
497
|
+
}
|
|
498
|
+
|
|
475
499
|
function normalizeValue(namespace: string, key: string, value: unknown): unknown {
|
|
476
500
|
if (value === undefined) throw new ValidationError("value required");
|
|
477
501
|
if (namespace === SPAWN_POLICY_NAMESPACE) return validateSpawnPolicy(key, value);
|
|
478
502
|
if (namespace === AGENT_PROFILE_NAMESPACE) return validateAgentProfile(key, value);
|
|
479
503
|
if (namespace === STEWARD_NAMESPACE) return validateStewardConfig(value);
|
|
480
504
|
if (namespace === INSIGHTS_NAMESPACE) return validateInsightsConfig(value);
|
|
505
|
+
if (namespace === WORKSPACE_NAMESPACE) return validateWorkspaceConfig(value);
|
|
481
506
|
if (JSON.stringify(value) === undefined) throw new ValidationError("value must be valid JSON");
|
|
482
507
|
return value;
|
|
483
508
|
}
|
|
484
509
|
|
|
485
510
|
export function getConfig<T = unknown>(namespace: string, key: string): ConfigEntry<T> | null {
|
|
486
|
-
const row = getDb().
|
|
511
|
+
const row = getDb().query("SELECT * FROM config WHERE namespace = ? AND key = ?").get(namespace, key) as ConfigRow | undefined;
|
|
487
512
|
return row ? rowToConfigEntry<T>(row) : null;
|
|
488
513
|
}
|
|
489
514
|
|
|
490
515
|
export function listConfig<T = unknown>(namespace: string): ConfigEntry<T>[] {
|
|
491
516
|
const rows = getDb()
|
|
492
|
-
.
|
|
517
|
+
.query("SELECT * FROM config WHERE namespace = ? ORDER BY key ASC")
|
|
493
518
|
.all(namespace) as ConfigRow[];
|
|
494
519
|
return rows.map(rowToConfigEntry<T>);
|
|
495
520
|
}
|
|
@@ -502,12 +527,12 @@ export function setConfig<T = unknown>(namespace: string, key: string, value: T,
|
|
|
502
527
|
|
|
503
528
|
getDb().transaction(() => {
|
|
504
529
|
if (existing) {
|
|
505
|
-
getDb().
|
|
530
|
+
getDb().query(`
|
|
506
531
|
INSERT INTO config_history (namespace, key, value, version, changed_at, changed_by)
|
|
507
532
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
508
533
|
`).run(existing.namespace, existing.key, JSON.stringify(existing.value), existing.version, now, updatedBy ?? null);
|
|
509
534
|
}
|
|
510
|
-
getDb().
|
|
535
|
+
getDb().query(`
|
|
511
536
|
INSERT INTO config (namespace, key, value, version, updated_at, updated_by)
|
|
512
537
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
513
538
|
ON CONFLICT(namespace, key) DO UPDATE SET
|
|
@@ -527,11 +552,11 @@ export function deleteConfig(namespace: string, key: string, updatedBy?: string)
|
|
|
527
552
|
if (!existing) return false;
|
|
528
553
|
const now = new Date().toISOString();
|
|
529
554
|
getDb().transaction(() => {
|
|
530
|
-
getDb().
|
|
555
|
+
getDb().query(`
|
|
531
556
|
INSERT INTO config_history (namespace, key, value, version, changed_at, changed_by)
|
|
532
557
|
VALUES (?, ?, ?, ?, ?, ?)
|
|
533
558
|
`).run(existing.namespace, existing.key, JSON.stringify(existing.value), existing.version, now, updatedBy ?? null);
|
|
534
|
-
getDb().
|
|
559
|
+
getDb().query("DELETE FROM config WHERE namespace = ? AND key = ?").run(namespace, key);
|
|
535
560
|
pruneConfigHistory(namespace, key);
|
|
536
561
|
})();
|
|
537
562
|
return true;
|
|
@@ -540,13 +565,13 @@ export function deleteConfig(namespace: string, key: string, updatedBy?: string)
|
|
|
540
565
|
export function getConfigHistory<T = unknown>(namespace: string, key: string, limit = CONFIG_HISTORY_LIMIT): ConfigHistoryEntry<T>[] {
|
|
541
566
|
const safeLimit = Math.min(Math.max(limit, 1), 500);
|
|
542
567
|
const rows = getDb()
|
|
543
|
-
.
|
|
568
|
+
.query("SELECT * FROM config_history WHERE namespace = ? AND key = ? ORDER BY version DESC, id DESC LIMIT ?")
|
|
544
569
|
.all(namespace, key, safeLimit) as ConfigHistoryRow[];
|
|
545
570
|
return rows.map(rowToConfigHistoryEntry<T>);
|
|
546
571
|
}
|
|
547
572
|
|
|
548
573
|
function pruneConfigHistory(namespace: string, key: string): void {
|
|
549
|
-
getDb().
|
|
574
|
+
getDb().query(`
|
|
550
575
|
DELETE FROM config_history
|
|
551
576
|
WHERE namespace = ? AND key = ? AND id NOT IN (
|
|
552
577
|
SELECT id FROM config_history
|
|
@@ -614,6 +639,40 @@ export function setInsightsConfig(value: unknown, updatedBy?: string): ConfigEnt
|
|
|
614
639
|
return setConfig(INSIGHTS_NAMESPACE, INSIGHTS_KEY, value as InsightsConfig, updatedBy);
|
|
615
640
|
}
|
|
616
641
|
|
|
642
|
+
/** Global workspace config, merged over defaults (always returns a usable value). */
|
|
643
|
+
export function getWorkspaceConfig(): WorkspaceConfig {
|
|
644
|
+
const entry = getConfig<Partial<WorkspaceConfig>>(WORKSPACE_NAMESPACE, WORKSPACE_KEY);
|
|
645
|
+
if (!entry) return { ...WORKSPACE_CONFIG_DEFAULTS };
|
|
646
|
+
return validateWorkspaceConfig({ ...WORKSPACE_CONFIG_DEFAULTS, ...entry.value });
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
export function getWorkspaceConfigEntry(): ConfigEntry<WorkspaceConfig> {
|
|
650
|
+
const entry = getConfig<WorkspaceConfig>(WORKSPACE_NAMESPACE, WORKSPACE_KEY);
|
|
651
|
+
return entry ?? {
|
|
652
|
+
namespace: WORKSPACE_NAMESPACE,
|
|
653
|
+
key: WORKSPACE_KEY,
|
|
654
|
+
value: { ...WORKSPACE_CONFIG_DEFAULTS },
|
|
655
|
+
version: 0,
|
|
656
|
+
updatedAt: "default",
|
|
657
|
+
updatedBy: "system",
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
export function setWorkspaceConfig(value: unknown, updatedBy?: string): ConfigEntry<WorkspaceConfig> {
|
|
662
|
+
return setConfig(WORKSPACE_NAMESPACE, WORKSPACE_KEY, value as WorkspaceConfig, updatedBy);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Spawn-param fragment carrying the global workspace symlink list to the orchestrator.
|
|
667
|
+
* Spread into every spawn command's params next to `agentProfile` so any isolated
|
|
668
|
+
* worktree — direct spawn, managed policy, restart, automation — provisions the same
|
|
669
|
+
* untracked paths. Returns `{}` when nothing is configured (keeps params lean).
|
|
670
|
+
*/
|
|
671
|
+
export function workspaceSpawnParams(): { workspaceSymlinks?: string[] } {
|
|
672
|
+
const { symlinkPaths } = getWorkspaceConfig();
|
|
673
|
+
return symlinkPaths.length ? { workspaceSymlinks: symlinkPaths } : {};
|
|
674
|
+
}
|
|
675
|
+
|
|
617
676
|
function builtInProfileEntry(profile: AgentProfile): ConfigEntry<AgentProfile> {
|
|
618
677
|
return {
|
|
619
678
|
namespace: AGENT_PROFILE_NAMESPACE,
|
|
@@ -649,13 +708,13 @@ export function deleteAgentProfile(name: string, updatedBy?: string): boolean {
|
|
|
649
708
|
}
|
|
650
709
|
|
|
651
710
|
export function getManagedAgentState(policyName: string): ManagedAgentState | null {
|
|
652
|
-
const row = getDb().
|
|
711
|
+
const row = getDb().query("SELECT * FROM managed_agent_state WHERE policy_name = ?").get(policyName) as ManagedAgentStateRow | undefined;
|
|
653
712
|
return row ? rowToManagedAgentState(row) : null;
|
|
654
713
|
}
|
|
655
714
|
|
|
656
715
|
function listManagedAgentStates(): ManagedAgentState[] {
|
|
657
716
|
const rows = getDb()
|
|
658
|
-
.
|
|
717
|
+
.query("SELECT * FROM managed_agent_state ORDER BY policy_name ASC")
|
|
659
718
|
.all() as ManagedAgentStateRow[];
|
|
660
719
|
return rows.map(rowToManagedAgentState);
|
|
661
720
|
}
|
|
@@ -664,7 +723,7 @@ export function upsertManagedAgentState(input: ManagedAgentStateInput): ManagedA
|
|
|
664
723
|
if (!VALID_MANAGED_STATUSES.includes(input.status)) throw new ValidationError("status must be a managed-agent status");
|
|
665
724
|
if (!VALID_PROVIDERS.includes(input.provider)) throw new ValidationError("provider must be claude or codex");
|
|
666
725
|
const now = input.updatedAt ?? Date.now();
|
|
667
|
-
getDb().
|
|
726
|
+
getDb().query(`
|
|
668
727
|
INSERT INTO managed_agent_state (
|
|
669
728
|
policy_name, status, agent_id, orchestrator_id, provider, tmux_session, spawn_request_id, workspace_id, workspace_path, workspace_branch,
|
|
670
729
|
last_spawn_at, last_stop_at, healthy_since, restart_count, consecutive_failures,
|
|
@@ -724,6 +783,6 @@ export function updateManagedAgentState(policyName: string, patch: ManagedAgentS
|
|
|
724
783
|
}
|
|
725
784
|
|
|
726
785
|
function deleteManagedAgentState(policyName: string): boolean {
|
|
727
|
-
const result = getDb().
|
|
786
|
+
const result = getDb().query("DELETE FROM managed_agent_state WHERE policy_name = ?").run(policyName);
|
|
728
787
|
return result.changes > 0;
|
|
729
788
|
}
|