EXTENSION seed
governance
Network governance visibility. Fetches governance policies from configured directories and surfaces them to the land-manager AI. Shows current seed version, minimum required version, recommended version, and compatibility status. Never forces updates. Only informs.
v1.0.1 by TreeOS Site 0 downloads 6 files 404 lines 11.8 KB published 38d ago
treeos ext install governance
View changelog

Manifest

Provides

  • routes
  • tools
  • jobs
  • 2 CLI commands

Optional

  • extensions: land-manager
SHA256: 2c56e347ebebaf4d72de93a4fe3e769539f4e2660c5e4eb703b395171cc9c0cd

Dependents

1 package depend on this

PackageTypeRelationship
treeos v1.0.1osstandalone

CLI Commands

CommandMethodDescription
governance-statusGETGovernance compliance status (cached)
governance-checkPOSTLive check against directory policies

Hooks

Listens To

  • enrichContext
  • afterBoot

Source Code

1import { SEED_VERSION } from "../../seed/version.js";
2
3// Cache governance state in memory. TTL 1 hour.
4let cachedState = null;
5let cacheTimestamp = 0;
6const CACHE_TTL = 60 * 60 * 1000; // 1 hour
7
8/**
9 * Parse comma-separated HORIZON_URL into an array of directory URLs.
10 */
11function getDirectoryUrls() {
12  return (process.env.HORIZON_URL || "")
13    .split(",")
14    .map((u) => u.trim())
15    .filter(Boolean);
16}
17
18/**
19 * Compare two semver strings. Returns -1, 0, or 1.
20 * Returns 0 if either string is not valid semver.
21 */
22export function compareSemver(a, b) {
23  const pa = String(a).match(/^(\d+)\.(\d+)\.(\d+)/);
24  const pb = String(b).match(/^(\d+)\.(\d+)\.(\d+)/);
25  if (!pa || !pb) return 0;
26
27  for (let i = 1; i <= 3; i++) {
28    const na = Number(pa[i]);
29    const nb = Number(pb[i]);
30    if (na < nb) return -1;
31    if (na > nb) return 1;
32  }
33  return 0;
34}
35
36/**
37 * Compute compatibility status for a directory's governance policy.
38 */
39function computeStatus(currentVersion, gov) {
40  if (!gov.minimumSeedVersion && !gov.recommendedSeedVersion) {
41    return "no_policy";
42  }
43
44  if (gov.minimumSeedVersion) {
45    if (compareSemver(currentVersion, gov.minimumSeedVersion) < 0) {
46      return "non_compliant";
47    }
48  }
49
50  if (gov.recommendedSeedVersion) {
51    if (compareSemver(currentVersion, gov.recommendedSeedVersion) < 0) {
52      return "advisory";
53    }
54  }
55
56  return "compliant";
57}
58
59/**
60 * Fetch governance data from all configured directories.
61 * Returns the governance state object.
62 */
63export async function refreshGovernance() {
64  const urls = getDirectoryUrls();
65  const directories = [];
66
67  const results = await Promise.allSettled(
68    urls.map(async (url) => {
69      const res = await fetch(`${url}/horizon/governance`, {
70        signal: AbortSignal.timeout(3000),
71      });
72      if (!res.ok) return { url, status: "unreachable", lastChecked: new Date().toISOString() };
73      const data = await res.json();
74      const gov = data.governance || {};
75      return {
76        url,
77        minimumSeedVersion: gov.minimumSeedVersion || null,
78        recommendedSeedVersion: gov.recommendedSeedVersion || null,
79        status: computeStatus(SEED_VERSION, gov),
80        lastChecked: new Date().toISOString(),
81      };
82    })
83  );
84
85  for (const r of results) {
86    if (r.status === "fulfilled") {
87      directories.push(r.value);
88    } else {
89      // Promise rejected (network error)
90      directories.push({
91        url: "unknown",
92        status: "unreachable",
93        lastChecked: new Date().toISOString(),
94      });
95    }
96  }
97
98  // Compute summary: worst status across all directories
99  const statusPriority = { non_compliant: 0, advisory: 1, unreachable: 2, no_policy: 3, compliant: 4 };
100  let worst = "compliant";
101  for (const d of directories) {
102    if ((statusPriority[d.status] ?? 5) < (statusPriority[worst] ?? 5)) {
103      worst = d.status;
104    }
105  }
106
107  cachedState = {
108    currentSeedVersion: SEED_VERSION,
109    directories,
110    summary: worst,
111  };
112  cacheTimestamp = Date.now();
113
114  return cachedState;
115}
116
117/**
118 * Get the cached governance state. Returns null if never fetched.
119 */
120export function getGovernanceState() {
121  if (cachedState && Date.now() - cacheTimestamp < CACHE_TTL) {
122    return cachedState;
123  }
124  return cachedState; // Return stale data rather than null. Refresh happens in background.
125}
126
127// ─────────────────────────────────────────────────────────────────────────
128// EXTENSION UPDATE CHECK
129// Cached separately from governance. Refreshed on the same hourly cycle.
130// ─────────────────────────────────────────────────────────────────────────
131
132let cachedUpdates = null;
133let updateCacheTimestamp = 0;
134
135/**
136 * Check installed extensions against the registry for available updates.
137 * Returns { updates: [{ name, installed, available }], checkedAt }
138 */
139export async function checkExtensionUpdates() {
140  const urls = getDirectoryUrls();
141  if (urls.length === 0) return { updates: [], checkedAt: new Date().toISOString() };
142
143  try {
144    const { getLoadedManifests } = await import("../../extensions/loader.js");
145    const manifests = getLoadedManifests();
146    const horizonUrl = urls[0];
147
148    const res = await fetch(`${horizonUrl}/extensions?limit=100`, {
149      signal: AbortSignal.timeout(3000),
150    });
151    if (!res.ok) return { updates: [], checkedAt: new Date().toISOString() };
152
153    const data = await res.json();
154    const registryExts = data.extensions || data || [];
155
156    const registryMap = new Map();
157    for (const ext of registryExts) {
158      const existing = registryMap.get(ext.name);
159      if (!existing || compareSemver(ext.version, existing) > 0) {
160        registryMap.set(ext.name, ext.version);
161      }
162    }
163
164    const updates = [];
165    for (const m of manifests) {
166      const registryVersion = registryMap.get(m.name);
167      if (!registryVersion) continue;
168      if (compareSemver(registryVersion, m.version) > 0) {
169        updates.push({ name: m.name, installed: m.version, available: registryVersion });
170      }
171    }
172
173    cachedUpdates = { updates, checkedAt: new Date().toISOString() };
174    updateCacheTimestamp = Date.now();
175    return cachedUpdates;
176  } catch {
177    return cachedUpdates || { updates: [], checkedAt: new Date().toISOString() };
178  }
179}
180
181/**
182 * Get cached extension update info.
183 */
184export function getExtensionUpdates() {
185  if (cachedUpdates && Date.now() - updateCacheTimestamp < CACHE_TTL) {
186    return cachedUpdates;
187  }
188  return cachedUpdates;
189}
190
1import { getGovernanceState, refreshGovernance, checkExtensionUpdates, getExtensionUpdates } from "./core.js";
2import buildTools from "./tools.js";
3import jobs from "./jobs.js";
4
5export async function init(core) {
6  // Enrich AI context so land-manager always knows governance state
7  // and available extension updates
8  core.hooks.register("enrichContext", async ({ context, node }) => {
9    // Only inject at land root (zone: land)
10    if (node?.systemRole !== "land-root" && node?.systemRole) return;
11
12    const state = getGovernanceState();
13    if (state && state.directories.length > 0) {
14      context.governance = state;
15    }
16
17    const updates = getExtensionUpdates();
18    if (updates?.updates?.length > 0) {
19      context.extensionUpdates = {
20        count: updates.updates.length,
21        available: updates.updates.slice(0, 5).map(u => `${u.name}: v${u.installed} -> v${u.available}`),
22        checkedAt: updates.checkedAt,
23      };
24    }
25  }, "governance");
26
27  // Refresh governance data and extension updates after boot
28  core.hooks.register("afterBoot", async () => {
29    try {
30      await refreshGovernance();
31    } catch {
32      // Non-fatal. Will retry on next hourly job cycle.
33    }
34    try {
35      await checkExtensionUpdates();
36    } catch {
37      // Non-fatal.
38    }
39  }, "governance");
40
41  const tools = buildTools();
42
43  return {
44    tools,
45    jobs,
46    modeTools: [
47      { modeKey: "land:manager", toolNames: ["governance-status", "governance-check"] },
48    ],
49    exports: { getGovernanceState, refreshGovernance, checkExtensionUpdates, getExtensionUpdates },
50  };
51}
52
1import { refreshGovernance, checkExtensionUpdates } from "./core.js";
2
3const REFRESH_INTERVAL = 60 * 60 * 1000; // 1 hour
4let intervalId = null;
5
6export default [
7  {
8    name: "governance-refresh",
9    start() {
10      if (intervalId) return;
11      intervalId = setInterval(async () => {
12        try { await refreshGovernance(); } catch {}
13        try { await checkExtensionUpdates(); } catch {}
14      }, REFRESH_INTERVAL);
15    },
16    stop() {
17      if (intervalId) {
18        clearInterval(intervalId);
19        intervalId = null;
20      }
21    },
22  },
23];
24
1export default {
2  name: "governance",
3  version: "1.0.1",
4  builtFor: "seed",
5  description:
6    "Network governance visibility. Fetches governance policies from configured directories " +
7    "and surfaces them to the land-manager AI. Shows current seed version, minimum required " +
8    "version, recommended version, and compatibility status. Never forces updates. Only informs.",
9
10  needs: {
11    services: [],
12    models: [],
13  },
14
15  optional: {
16    extensions: ["land-manager"],
17  },
18
19  provides: {
20    routes: "./routes.js",
21    tools: true,
22    jobs: true,
23    hooks: {
24      listens: ["enrichContext", "afterBoot"],
25    },
26    cli: [
27      { command: "governance-status", scope: ["land"], description: "Governance compliance status (cached)", method: "GET", endpoint: "/land/governance" },
28      { command: "governance-check", scope: ["land"], description: "Live check against directory policies", method: "POST", endpoint: "/land/governance/check" },
29    ],
30  },
31};
32
1import { Router } from "express";
2import { getGovernanceState, refreshGovernance } from "./core.js";
3
4const router = Router();
5
6/**
7 * GET /api/v1/land/governance
8 * Returns the cached governance state. Used by CLI.
9 */
10router.get("/land/governance", (req, res) => {
11  const state = getGovernanceState();
12  if (!state) {
13    return res.json({ status: "ok", data: { message: "Governance data not yet fetched" } });
14  }
15  return res.json({ status: "ok", data: state });
16});
17
18/**
19 * POST /api/v1/land/governance/check
20 * Live check against directory policies (bypasses cache).
21 */
22router.post("/land/governance/check", async (req, res) => {
23  try {
24    const state = await refreshGovernance();
25    return res.json({ status: "ok", data: state });
26  } catch (err) {
27    return res.status(500).json({ status: "error", error: { code: "INTERNAL", message: err.message } });
28  }
29});
30
31export default router;
32
1import { getGovernanceState, refreshGovernance } from "./core.js";
2
3export default function buildTools() {
4  return [
5    {
6      name: "governance-status",
7      description:
8        "Show governance status for this land. " +
9        "Returns current seed version, each directory's governance policy, " +
10        "and compatibility status (compliant, advisory, non_compliant, no_policy, unreachable).",
11      inputSchema: {
12        type: "object",
13        properties: {},
14      },
15      handler: async () => {
16        const state = getGovernanceState();
17        if (!state || state.directories.length === 0) {
18          return {
19            content: [{ type: "text", text: "No governance data available. No directories configured or data not yet fetched." }],
20          };
21        }
22
23        const lines = [`Seed version: ${state.currentSeedVersion}`, `Overall status: ${state.summary}`, ""];
24
25        for (const d of state.directories) {
26          lines.push(`Directory: ${d.url}`);
27          lines.push(`  Status: ${d.status}`);
28          if (d.minimumSeedVersion) lines.push(`  Minimum version: ${d.minimumSeedVersion}`);
29          if (d.recommendedSeedVersion) lines.push(`  Recommended version: ${d.recommendedSeedVersion}`);
30          lines.push(`  Last checked: ${d.lastChecked}`);
31          lines.push("");
32        }
33
34        return {
35          content: [{ type: "text", text: lines.join("\n") }],
36        };
37      },
38    },
39    {
40      name: "governance-check",
41      description:
42        "Force a fresh governance check against all configured directories. " +
43        "Bypasses cache and returns real-time data.",
44      inputSchema: {
45        type: "object",
46        properties: {},
47      },
48      handler: async () => {
49        const state = await refreshGovernance();
50        if (!state || state.directories.length === 0) {
51          return {
52            content: [{ type: "text", text: "No directories configured. Set HORIZON_URL to enable governance checks." }],
53          };
54        }
55
56        const lines = [`Seed version: ${state.currentSeedVersion}`, `Overall status: ${state.summary}`, ""];
57
58        for (const d of state.directories) {
59          lines.push(`Directory: ${d.url}`);
60          lines.push(`  Status: ${d.status}`);
61          if (d.minimumSeedVersion) lines.push(`  Minimum version: ${d.minimumSeedVersion}`);
62          if (d.recommendedSeedVersion) lines.push(`  Recommended version: ${d.recommendedSeedVersion}`);
63          lines.push(`  Last checked: ${d.lastChecked}`);
64          lines.push("");
65        }
66
67        return {
68          content: [{ type: "text", text: lines.join("\n") }],
69        };
70      },
71    },
72  ];
73}
74

Versions

Version Published Downloads
1.0.1 38d ago 0
1.0.0 48d ago 0
0 stars
0 flags
React from the CLI: treeos ext star governance

Comments

Loading comments...

Post comments from the CLI: treeos ext comment governance "your comment"
Max 3 comments per extension. One star and one flag per user.