@connexum/ai-governance 1.0.0-beta.21 → 1.0.0-beta.23
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/dist/cli/agent-dir-scanner.d.ts +32 -0
- package/dist/cli/agent-dir-scanner.d.ts.map +1 -1
- package/dist/cli/agent-dir-scanner.js +52 -0
- package/dist/cli/agent-dir-scanner.js.map +1 -1
- package/dist/cli/governance-md-renderer.d.ts +50 -0
- package/dist/cli/governance-md-renderer.d.ts.map +1 -0
- package/dist/cli/governance-md-renderer.js +185 -0
- package/dist/cli/governance-md-renderer.js.map +1 -0
- package/dist/cli/governance-projection-writer.d.ts +88 -0
- package/dist/cli/governance-projection-writer.d.ts.map +1 -0
- package/dist/cli/governance-projection-writer.js +291 -0
- package/dist/cli/governance-projection-writer.js.map +1 -0
- package/dist/cli/index.d.ts +85 -0
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +343 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/per-folder-identity.d.ts +79 -0
- package/dist/cli/per-folder-identity.d.ts.map +1 -0
- package/dist/cli/per-folder-identity.js +321 -0
- package/dist/cli/per-folder-identity.js.map +1 -0
- package/dist/cli/sync.d.ts +193 -0
- package/dist/cli/sync.d.ts.map +1 -0
- package/dist/cli/sync.js +1094 -0
- package/dist/cli/sync.js.map +1 -0
- package/dist/cli/wrap-shim-generator.d.ts +33 -0
- package/dist/cli/wrap-shim-generator.d.ts.map +1 -1
- package/dist/cli/wrap-shim-generator.js +93 -8
- package/dist/cli/wrap-shim-generator.js.map +1 -1
- package/dist/esm/cli/agent-dir-scanner.js +52 -0
- package/dist/esm/cli/agent-dir-scanner.js.map +1 -1
- package/dist/esm/cli/governance-md-renderer.js +182 -0
- package/dist/esm/cli/governance-md-renderer.js.map +1 -0
- package/dist/esm/cli/governance-projection-writer.js +253 -0
- package/dist/esm/cli/governance-projection-writer.js.map +1 -0
- package/dist/esm/cli/index.js +343 -3
- package/dist/esm/cli/index.js.map +1 -1
- package/dist/esm/cli/per-folder-identity.js +283 -0
- package/dist/esm/cli/per-folder-identity.js.map +1 -0
- package/dist/esm/cli/sync.js +1054 -0
- package/dist/esm/cli/sync.js.map +1 -0
- package/dist/esm/cli/wrap-shim-generator.js +92 -8
- package/dist/esm/cli/wrap-shim-generator.js.map +1 -1
- package/dist/esm/governance/governance-projection-canonical.js +101 -0
- package/dist/esm/governance/governance-projection-canonical.js.map +1 -0
- package/dist/governance/governance-projection-canonical.d.ts +104 -0
- package/dist/governance/governance-projection-canonical.d.ts.map +1 -0
- package/dist/governance/governance-projection-canonical.js +141 -0
- package/dist/governance/governance-projection-canonical.js.map +1 -0
- package/dist/hooks/audit-logger.sh +108 -10
- package/package.json +1 -1
- package/src/hooks/audit-logger.sh +108 -10
package/dist/esm/cli/index.js
CHANGED
|
@@ -22,7 +22,9 @@ import { JwtValidator } from '../license/index.js';
|
|
|
22
22
|
import { BUILT_IN_PACKS } from '../governed-agent.js';
|
|
23
23
|
import { scanAgentDirectory } from './agent-dir-scanner.js';
|
|
24
24
|
import { normalizeAgentDir } from './normalize-agent-dir.js';
|
|
25
|
-
import { writeWrapShims } from './wrap-shim-generator.js';
|
|
25
|
+
import { writeWrapShims, regenerateShimAgentIds } from './wrap-shim-generator.js';
|
|
26
|
+
// Identity RFC B1 (per-folder identity files) — Pass-1 wiring (init --link).
|
|
27
|
+
import { writePerFolderIdentities, buildPerFolderIdentityInputs } from './per-folder-identity.js';
|
|
26
28
|
import { preflightAgents } from './preflight.js';
|
|
27
29
|
import { formatPreflightReport } from './preflight-report.js';
|
|
28
30
|
/**
|
|
@@ -1208,6 +1210,25 @@ async function interactiveInit(projectDir, opts = {}) {
|
|
|
1208
1210
|
writeVendorCodeToConfig(projectDir, '', true);
|
|
1209
1211
|
process.stdout.write('Offline mode recorded in .governance.json\n');
|
|
1210
1212
|
}
|
|
1213
|
+
// TS-010 KEY-TRUST: pin the org public key from the server into
|
|
1214
|
+
// .governance.json immediately after the config is written.
|
|
1215
|
+
// The key is fetched over authenticated TLS from the same gov-server that
|
|
1216
|
+
// issued the serviceToken. Subsequent `ai-governance sync` calls verify
|
|
1217
|
+
// bundle signatures against this pinned key (never the bundle-embedded key).
|
|
1218
|
+
// Non-fatal: advisory if unavailable (Invariant 2), but sync will fail
|
|
1219
|
+
// closed (bundle rejected) until the key is pinned.
|
|
1220
|
+
if (opts.runtime?.govServerUrl && opts.runtime?.orgId) {
|
|
1221
|
+
const { fetchAndPinOrgPublicKey } = require('./sync.js');
|
|
1222
|
+
const pinned = fetchAndPinOrgPublicKey(opts.runtime.govServerUrl, opts.runtime.orgId, configPath);
|
|
1223
|
+
if (pinned) {
|
|
1224
|
+
process.stdout.write(`Org public key pinned for offline bundle verification.\n`);
|
|
1225
|
+
}
|
|
1226
|
+
else {
|
|
1227
|
+
process.stderr.write('WARNING: could not fetch org public key for pinning. ' +
|
|
1228
|
+
'`ai-governance sync` will reject bundles until the key is pinned. ' +
|
|
1229
|
+
'Re-run `ai-governance init` when the server is reachable.\n');
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1211
1232
|
// Install hooks
|
|
1212
1233
|
const { installed, errors } = installHooks(projectDir, config);
|
|
1213
1234
|
process.stdout.write(`Hooks installed: ${installed}\n`);
|
|
@@ -1689,6 +1710,109 @@ export function writeGovernanceMcpSuggest(projectDir, tenantId) {
|
|
|
1689
1710
|
].join('\n'));
|
|
1690
1711
|
return path.relative(projectDir, mcpSuggestPath);
|
|
1691
1712
|
}
|
|
1713
|
+
/**
|
|
1714
|
+
* TS-002 CLI half — consume the registered[] list returned by register-fleet
|
|
1715
|
+
* and write a per-agent identity block into the project's .governance.json.
|
|
1716
|
+
*
|
|
1717
|
+
* Shape written:
|
|
1718
|
+
* ```json
|
|
1719
|
+
* {
|
|
1720
|
+
* "agents": [
|
|
1721
|
+
* { "localId": "auto-...", "agentId": "srv-uuid", "passportId": "pp-...",
|
|
1722
|
+
* "serviceToken": "eyJ...", "filePath": "src/agents/billing.py" }
|
|
1723
|
+
* ],
|
|
1724
|
+
* "runtime": {
|
|
1725
|
+
* "agentId": "<first-registered-agentId>",
|
|
1726
|
+
* "serviceToken": "<first-registered-serviceToken>",
|
|
1727
|
+
* ...rest of existing runtime block
|
|
1728
|
+
* }
|
|
1729
|
+
* }
|
|
1730
|
+
* ```
|
|
1731
|
+
*
|
|
1732
|
+
* Back-compat guarantee: runtime.agentId + runtime.serviceToken are updated to
|
|
1733
|
+
* the first registered agent so the existing audit-logger.sh → /cluster-events
|
|
1734
|
+
* push path (which reads these two flat fields) continues to work for the
|
|
1735
|
+
* single-agent case. The per-agent `agents[]` array is the source of truth for
|
|
1736
|
+
* multi-agent deployments; future per-agent attribution reads from there.
|
|
1737
|
+
*
|
|
1738
|
+
* Null-passport and null-serviceToken entries are written without throwing
|
|
1739
|
+
* (Invariant 2 — advisory, never blocking). A null serviceToken from agent N
|
|
1740
|
+
* does NOT overwrite the runtime.serviceToken that was already set.
|
|
1741
|
+
*
|
|
1742
|
+
* Idempotent: calling twice with the same input upserts by localId — does not
|
|
1743
|
+
* duplicate entries.
|
|
1744
|
+
*
|
|
1745
|
+
* @param projectDir Absolute path to the directory containing .governance.json
|
|
1746
|
+
* @param registered Array from register-fleet response.registered[]
|
|
1747
|
+
* @param localAgents Locally scanned DetectedAgent list (used to resolve filePath by localId)
|
|
1748
|
+
*/
|
|
1749
|
+
export function writePerAgentIdentities(projectDir, registered, localAgents, opts = {}) {
|
|
1750
|
+
if (registered.length === 0)
|
|
1751
|
+
return;
|
|
1752
|
+
const configPath = path.join(projectDir, '.governance.json');
|
|
1753
|
+
// Read existing config — must preserve all existing keys (packs, hooks, etc.)
|
|
1754
|
+
let config = {};
|
|
1755
|
+
if (fs.existsSync(configPath)) {
|
|
1756
|
+
try {
|
|
1757
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1758
|
+
}
|
|
1759
|
+
catch { /* fresh config — start empty */ }
|
|
1760
|
+
}
|
|
1761
|
+
// Build a quick-lookup: localId → filePath from the local scan.
|
|
1762
|
+
// For entries where localId is absent, fall back to agentId as key.
|
|
1763
|
+
const filePathByLocalId = new Map();
|
|
1764
|
+
for (const la of localAgents) {
|
|
1765
|
+
filePathByLocalId.set(la.agentId, la.filePath);
|
|
1766
|
+
}
|
|
1767
|
+
// Build the new per-agent entries.
|
|
1768
|
+
const newEntries = registered.map((r) => {
|
|
1769
|
+
const key = r.localId ?? r.agentId;
|
|
1770
|
+
return {
|
|
1771
|
+
localId: key,
|
|
1772
|
+
agentId: r.agentId,
|
|
1773
|
+
passportId: r.passportId,
|
|
1774
|
+
serviceToken: r.serviceToken,
|
|
1775
|
+
filePath: filePathByLocalId.get(key),
|
|
1776
|
+
};
|
|
1777
|
+
});
|
|
1778
|
+
// Upsert into existing agents[] (idempotent — replace by localId).
|
|
1779
|
+
const existingEntries = Array.isArray(config['agents'])
|
|
1780
|
+
? config['agents']
|
|
1781
|
+
: [];
|
|
1782
|
+
const merged = [...existingEntries];
|
|
1783
|
+
for (const entry of newEntries) {
|
|
1784
|
+
const idx = merged.findIndex((e) => e.localId === entry.localId);
|
|
1785
|
+
if (idx >= 0) {
|
|
1786
|
+
merged[idx] = entry;
|
|
1787
|
+
}
|
|
1788
|
+
else {
|
|
1789
|
+
merged.push(entry);
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
config['agents'] = merged;
|
|
1793
|
+
// Back-compat: update runtime.agentId + runtime.serviceToken to the first
|
|
1794
|
+
// registered agent so the audit-logger.sh single-agent push path keeps working.
|
|
1795
|
+
// Only update runtime.serviceToken if the first agent's token is non-null
|
|
1796
|
+
// (do not overwrite a valid existing token with null).
|
|
1797
|
+
const first = newEntries[0];
|
|
1798
|
+
if (first) {
|
|
1799
|
+
const runtime = config['runtime'] ?? {};
|
|
1800
|
+
runtime['agentId'] = first.agentId;
|
|
1801
|
+
if (first.serviceToken !== null) {
|
|
1802
|
+
runtime['serviceToken'] = first.serviceToken;
|
|
1803
|
+
}
|
|
1804
|
+
if (opts.agentDir) {
|
|
1805
|
+
runtime['agentDir'] = opts.agentDir;
|
|
1806
|
+
}
|
|
1807
|
+
config['runtime'] = runtime;
|
|
1808
|
+
}
|
|
1809
|
+
// Write atomically — mode 0o600 (service tokens are secrets, Invariant 10).
|
|
1810
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
1811
|
+
try {
|
|
1812
|
+
fs.chmodSync(configPath, 0o600);
|
|
1813
|
+
}
|
|
1814
|
+
catch { /* best-effort on Windows */ }
|
|
1815
|
+
}
|
|
1692
1816
|
// --- Non-interactive init (CI mode) ---
|
|
1693
1817
|
export async function nonInteractiveInit(projectDir, opts) {
|
|
1694
1818
|
const licenseKey = opts.license || 'free';
|
|
@@ -2437,6 +2561,10 @@ Usage:
|
|
|
2437
2561
|
[--archive-dir <dir>] Archive destination (else delete)
|
|
2438
2562
|
[--dry-run] Report without acting
|
|
2439
2563
|
[--json] Machine-readable output
|
|
2564
|
+
ai-governance sync Pull server governance bundle to local .governance.json
|
|
2565
|
+
[--apply] Write changes (default: dry-run, shows diff only)
|
|
2566
|
+
[--agent <id>] Sync a specific agent only
|
|
2567
|
+
[--gov-server-url <url>] Override gov server URL
|
|
2440
2568
|
ai-governance rotate-token Rotate the runtime service token
|
|
2441
2569
|
Reads .governance.json runtime.serviceToken,
|
|
2442
2570
|
requests a fresh token from the gov-server,
|
|
@@ -2762,7 +2890,74 @@ if (isDirectRun) {
|
|
|
2762
2890
|
if (runtimeConfig?.serviceToken && runtimeConfig?.govServerUrl && detectedAgents.length > 0) {
|
|
2763
2891
|
try {
|
|
2764
2892
|
const { spawnSync } = require('node:child_process');
|
|
2893
|
+
// TS-009: capture project identity so the server can link agents to
|
|
2894
|
+
// the customer's actual project for audit roll-up in the portal.
|
|
2895
|
+
//
|
|
2896
|
+
// projectId: deterministic hash of the agent-dir absolute path.
|
|
2897
|
+
// Uses SHA-256(resolvedAgentDir) → first 16 hex chars. Stable across
|
|
2898
|
+
// re-runs on the same machine for the same project. If the customer
|
|
2899
|
+
// moves the repo, a new projectId is generated (a new project entity).
|
|
2900
|
+
//
|
|
2901
|
+
// projectName: basename of resolvedAgentDir (human-readable). If git
|
|
2902
|
+
// is available and resolvedAgentDir is inside a repo, use the repo
|
|
2903
|
+
// name (basename of the top-level). Falls back to dir basename.
|
|
2904
|
+
//
|
|
2905
|
+
// origin: git remote 'origin' URL, read locally by the CLI from the
|
|
2906
|
+
// customer's repo. The client's CLI reads its own git remote — we
|
|
2907
|
+
// NEVER scan or poll their infrastructure (Invariant 10: push-receive).
|
|
2908
|
+
// Only sent when git is available and the remote is set; never fails.
|
|
2909
|
+
//
|
|
2910
|
+
// Advisory (Invariant 2): any failure in this block produces null
|
|
2911
|
+
// values — the fleet payload is always sent and registration never
|
|
2912
|
+
// fails because of a missing project block.
|
|
2913
|
+
const captureProjectIdentity = () => {
|
|
2914
|
+
let origin;
|
|
2915
|
+
let projectName = path.basename(resolvedAgentDir);
|
|
2916
|
+
let gitRootDir;
|
|
2917
|
+
// Attempt to read git remote origin and repo name (client reads
|
|
2918
|
+
// its own git — Invariant 10 respected).
|
|
2919
|
+
try {
|
|
2920
|
+
const gitRemote = spawnSync('git', ['-C', resolvedAgentDir, 'remote', 'get-url', 'origin'], { encoding: 'utf8', timeout: 3000 });
|
|
2921
|
+
if (gitRemote.status === 0 && gitRemote.stdout) {
|
|
2922
|
+
origin = gitRemote.stdout.trim();
|
|
2923
|
+
// Derive projectName from the repo name embedded in the origin URL.
|
|
2924
|
+
// Works for both SSH (git@github.com:org/repo.git) and HTTPS
|
|
2925
|
+
// (https://github.com/org/repo.git). Strip trailing .git.
|
|
2926
|
+
const match = origin.match(/[\/:]([\w.-]+?)(?:\.git)?$/);
|
|
2927
|
+
if (match && match[1]) {
|
|
2928
|
+
projectName = match[1];
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
catch { /* git not available or not a git repo — use dir basename */ }
|
|
2933
|
+
try {
|
|
2934
|
+
// Best-effort: get the git top-level for a more stable projectId
|
|
2935
|
+
const gitRoot = spawnSync('git', ['-C', resolvedAgentDir, 'rev-parse', '--show-toplevel'], { encoding: 'utf8', timeout: 3000 });
|
|
2936
|
+
if (gitRoot.status === 0 && gitRoot.stdout) {
|
|
2937
|
+
gitRootDir = gitRoot.stdout.trim();
|
|
2938
|
+
// Override projectName with repo root basename when git is available
|
|
2939
|
+
const rootBasename = path.basename(gitRootDir);
|
|
2940
|
+
if (rootBasename)
|
|
2941
|
+
projectName = rootBasename;
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
catch { /* best-effort */ }
|
|
2945
|
+
// projectId: deterministic hash of the absolute project root dir
|
|
2946
|
+
// (git root when available, otherwise resolvedAgentDir). This gives
|
|
2947
|
+
// all register-fleet runs from the same repo the same projectId.
|
|
2948
|
+
const hashInput = gitRootDir ?? resolvedAgentDir;
|
|
2949
|
+
const projectId = 'proj-' + crypto.createHash('sha256').update(hashInput).digest('hex').slice(0, 16);
|
|
2950
|
+
return { projectId, projectName, ...(origin ? { origin } : {}) };
|
|
2951
|
+
};
|
|
2952
|
+
let projectIdentity;
|
|
2953
|
+
try {
|
|
2954
|
+
projectIdentity = captureProjectIdentity();
|
|
2955
|
+
}
|
|
2956
|
+
catch {
|
|
2957
|
+
// Advisory: project identity capture failure must never block fleet registration.
|
|
2958
|
+
}
|
|
2765
2959
|
const fleetPayload = {
|
|
2960
|
+
...(projectIdentity ? { project: projectIdentity } : {}),
|
|
2766
2961
|
agents: detectedAgents.map((a) => ({
|
|
2767
2962
|
localId: a.agentId,
|
|
2768
2963
|
name: a.name ?? undefined,
|
|
@@ -2774,6 +2969,17 @@ if (isDirectRun) {
|
|
|
2774
2969
|
// so the platform's OWASP/ASI analysis grades real tool surface.
|
|
2775
2970
|
tools: a.tools && a.tools.length > 0 ? a.tools : undefined,
|
|
2776
2971
|
mcpServers: a.mcpServers && a.mcpServers.length > 0 ? a.mcpServers : undefined,
|
|
2972
|
+
// TS-008 lossless capture (ticket A2): the scanner already
|
|
2973
|
+
// parses these from manifest.json; sending them closes the
|
|
2974
|
+
// model/version/vertical/allowed_domains/toolDetails/
|
|
2975
|
+
// max_concurrent_actions = null gap on the registered agent
|
|
2976
|
+
// (server validates + persists them — FleetAgentInput).
|
|
2977
|
+
model_identifier: a.model_identifier,
|
|
2978
|
+
version: a.version,
|
|
2979
|
+
vertical: a.vertical,
|
|
2980
|
+
allowed_domains: a.allowed_domains && a.allowed_domains.length > 0 ? a.allowed_domains : undefined,
|
|
2981
|
+
toolDetails: a.toolDetails && a.toolDetails.length > 0 ? a.toolDetails : undefined,
|
|
2982
|
+
max_concurrent_actions: a.max_concurrent_actions,
|
|
2777
2983
|
})),
|
|
2778
2984
|
};
|
|
2779
2985
|
const fleetResult = spawnSync('curl', [
|
|
@@ -2792,9 +2998,109 @@ if (isDirectRun) {
|
|
|
2792
2998
|
const resp = JSON.parse(fleetResult.stdout);
|
|
2793
2999
|
if (typeof resp.registeredCount === 'number') {
|
|
2794
3000
|
fleetRegistered = true;
|
|
2795
|
-
process.stdout.write(`\n✅
|
|
3001
|
+
process.stdout.write(`\n✅ Detected ${resp.registeredCount} agent(s) on the platform` +
|
|
2796
3002
|
(resp.skippedCount ? ` (${resp.skippedCount} already registered, skipped)` : '') +
|
|
2797
|
-
` — check your dashboard.\n`
|
|
3003
|
+
` — check your dashboard.\n` +
|
|
3004
|
+
// Slice 3c/3d (Decision 4): passports + runtime tokens are issued
|
|
3005
|
+
// at payment activation, not at detection. Tell the customer that
|
|
3006
|
+
// runtime push is unavailable until they Confirm + pay, and how to
|
|
3007
|
+
// activate it afterward.
|
|
3008
|
+
` ⏳ Runtime governance push is UNAVAILABLE until you Confirm + pay in the dashboard\n` +
|
|
3009
|
+
` (detected agents are not yet activated). After you Confirm, run\n` +
|
|
3010
|
+
` \`ai-governance sync --apply\` to activate each agent's runtime push.\n`);
|
|
3011
|
+
// TS-002 CLI half: persist per-agent identities into .governance.json.
|
|
3012
|
+
// Each agent gets its own serviceToken (sub=agent:<agentId>) so future
|
|
3013
|
+
// per-agent audit pushes carry the correct identity. The runtime block
|
|
3014
|
+
// is updated to the first agent for single-agent backward-compat.
|
|
3015
|
+
// Non-fatal (Invariant 2): if this write fails governance still runs.
|
|
3016
|
+
// Ghost-fix (#1717) fail-loud: print WHY any agent failed to
|
|
3017
|
+
// register — a bare count hid the b60d6cbc-class failures.
|
|
3018
|
+
if (Array.isArray(resp.failures) && resp.failures.length > 0) {
|
|
3019
|
+
for (const f of resp.failures) {
|
|
3020
|
+
process.stderr.write(` ❌ ${f.name}: registration failed — ${f.reason}\n`);
|
|
3021
|
+
}
|
|
3022
|
+
process.stderr.write(' Re-run the install command to retry the failed agent(s).\n');
|
|
3023
|
+
}
|
|
3024
|
+
if (typeof resp.healedGhostCount === 'number' && resp.healedGhostCount > 0) {
|
|
3025
|
+
process.stdout.write(` ♻ Healed ${resp.healedGhostCount} previously-stuck agent registration(s).\n`);
|
|
3026
|
+
}
|
|
3027
|
+
if (Array.isArray(resp.registered) && resp.registered.length > 0) {
|
|
3028
|
+
try {
|
|
3029
|
+
// Shield F1 (#1720, MEDIUM): server agentIds are BAKED into
|
|
3030
|
+
// executable shim files via raw template interpolation and
|
|
3031
|
+
// into URL paths — gate them against the canonical format
|
|
3032
|
+
// at the trust boundary before ANY downstream use. A
|
|
3033
|
+
// non-matching id from a compromised/MITM'd response is
|
|
3034
|
+
// dropped with a visible WARN, never interpolated.
|
|
3035
|
+
const SAFE_SERVER_AGENT_ID = /^agent-[0-9a-f]{8}$/;
|
|
3036
|
+
const safeRegistered = resp.registered.filter((r) => {
|
|
3037
|
+
if (SAFE_SERVER_AGENT_ID.test(r.agentId))
|
|
3038
|
+
return true;
|
|
3039
|
+
process.stderr.write(` [WARN] server returned a non-canonical agentId (${JSON.stringify(r.agentId).slice(0, 60)}) — entry dropped.\n`);
|
|
3040
|
+
return false;
|
|
3041
|
+
});
|
|
3042
|
+
const localRefs = detectedAgents.map((a) => ({
|
|
3043
|
+
agentId: a.agentId,
|
|
3044
|
+
filePath: a.filePath,
|
|
3045
|
+
}));
|
|
3046
|
+
// Persist the scan root so sync passes can resolve the
|
|
3047
|
+
// SCAN-ROOT-relative agents[].filePath to real folders.
|
|
3048
|
+
const agentDirRel = path.relative(projectDir, resolvedAgentDir) || '.';
|
|
3049
|
+
writePerAgentIdentities(projectDir, safeRegistered, localRefs, {
|
|
3050
|
+
agentDir: path.isAbsolute(agentDirRel) ? resolvedAgentDir : agentDirRel,
|
|
3051
|
+
});
|
|
3052
|
+
process.stdout.write(`[CLI] per-agent identities written to .governance.json ` +
|
|
3053
|
+
`(${safeRegistered.length} agent(s)).\n`);
|
|
3054
|
+
// Identity RFC B1 (Pass 1): land each agent's identity in
|
|
3055
|
+
// ITS OWN folder — <agent>/.connexum/identity.json. No
|
|
3056
|
+
// secret at this pass (tokens are minted at payment
|
|
3057
|
+
// activation), so this writes metadata regardless of git
|
|
3058
|
+
// state. Folder root = the SCAN root, not projectDir.
|
|
3059
|
+
const folderInputs = buildPerFolderIdentityInputs(safeRegistered, localRefs, {
|
|
3060
|
+
installationId: resp.installationId,
|
|
3061
|
+
signingKeyId: resp.signingKeyId,
|
|
3062
|
+
govServerUrl: resp.govServerUrl ?? runtimeConfig.govServerUrl,
|
|
3063
|
+
orgId: resp.orgId ?? runtimeConfig.orgId,
|
|
3064
|
+
}, new Date().toISOString());
|
|
3065
|
+
const folderRes = writePerFolderIdentities(resolvedAgentDir, folderInputs, {
|
|
3066
|
+
warn: (m) => process.stderr.write(m + '\n'),
|
|
3067
|
+
});
|
|
3068
|
+
process.stdout.write(`[CLI] per-folder identity files: ${folderRes.written.length} written` +
|
|
3069
|
+
(folderRes.skipped.length > 0 ? `, ${folderRes.skipped.length} skipped` : '') + '.\n');
|
|
3070
|
+
// Identity RFC C1 (Option B): regenerate each shim with
|
|
3071
|
+
// the SERVER agentId. Without this every shim keeps the
|
|
3072
|
+
// baked local auto-* id and all runtime signals collapse
|
|
3073
|
+
// onto ids the platform never issued. Pristine shims are
|
|
3074
|
+
// rebaked; edited shims get a VISIBLE warn (never a
|
|
3075
|
+
// silent skip — Stern BLOCK-2).
|
|
3076
|
+
const serverIdByLocalId = new Map();
|
|
3077
|
+
for (const r of safeRegistered) {
|
|
3078
|
+
if (r.localId)
|
|
3079
|
+
serverIdByLocalId.set(r.localId, r.agentId);
|
|
3080
|
+
}
|
|
3081
|
+
const regenResults = regenerateShimAgentIds(detectedAgents, serverIdByLocalId, resolvedAgentDir, (msg) => process.stdout.write(msg + '\n'));
|
|
3082
|
+
const rebaked = regenResults.filter((r) => r.status === 'regenerated' || r.status === 'verified').length;
|
|
3083
|
+
const stuck = regenResults.filter((r) => r.status === 'skipped-edited' || r.status === 'failed');
|
|
3084
|
+
process.stdout.write(`[CLI] shim identity rebake: ${rebaked} on server ids` +
|
|
3085
|
+
(stuck.length > 0 ? `, ${stuck.length} NEED ATTENTION (listed above)` : '') + '.\n');
|
|
3086
|
+
if (stuck.length > 0) {
|
|
3087
|
+
// Stern F1 (#1720): the recovery path must be in the
|
|
3088
|
+
// operator-facing summary, not buried in per-file WARNs.
|
|
3089
|
+
process.stderr.write('⚠️ The agent(s) listed above still emit their LOCAL id at runtime — their\n' +
|
|
3090
|
+
' signals will not attach to the registered platform identity until fixed.\n' +
|
|
3091
|
+
' Recovery: if you did NOT edit the .governed file yourself, DELETE it and\n' +
|
|
3092
|
+
' re-run this install command — it will be regenerated with the correct id.\n' +
|
|
3093
|
+
' If you did edit it, set its AGENT_ID to the server id shown above.\n');
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
catch (identityErr) {
|
|
3097
|
+
// Non-fatal — fleet is registered on the platform; local identity
|
|
3098
|
+
// write failure must not undo that success (Invariant 2).
|
|
3099
|
+
process.stderr.write(`[CLI] per-agent identity write failed (non-fatal): ` +
|
|
3100
|
+
`${identityErr.message}. ` +
|
|
3101
|
+
`Agents are registered on the platform; re-run init to retry the write.\n`);
|
|
3102
|
+
}
|
|
3103
|
+
}
|
|
2798
3104
|
}
|
|
2799
3105
|
else if (resp.error) {
|
|
2800
3106
|
process.stderr.write(`[CLI] register-fleet: server error: ${resp.error}\n`);
|
|
@@ -2885,6 +3191,27 @@ if (isDirectRun) {
|
|
|
2885
3191
|
runtime: runtimeConfig,
|
|
2886
3192
|
agentDir: agentDirArg,
|
|
2887
3193
|
});
|
|
3194
|
+
// TS-010 KEY-TRUST: pin the org public key from the server into
|
|
3195
|
+
// .governance.json immediately after init writes the config file.
|
|
3196
|
+
// This is the trusted pinning event — the key is fetched over the same
|
|
3197
|
+
// authenticated TLS channel that issued the runtimeConfig exchange.
|
|
3198
|
+
// Subsequent `ai-governance sync` calls verify bundle signatures against
|
|
3199
|
+
// this pinned key. Non-fatal: advisory if unavailable (Invariant 2), but
|
|
3200
|
+
// sync will fail closed until the key is pinned.
|
|
3201
|
+
if (runtimeConfig?.govServerUrl && runtimeConfig?.orgId) {
|
|
3202
|
+
// fetchAndPinOrgPublicKey is imported from sync.ts
|
|
3203
|
+
const { fetchAndPinOrgPublicKey } = require('./sync.js');
|
|
3204
|
+
const configPath = path.join(projectDir, '.governance.json');
|
|
3205
|
+
const pinned = fetchAndPinOrgPublicKey(runtimeConfig.govServerUrl, runtimeConfig.orgId, configPath);
|
|
3206
|
+
if (pinned) {
|
|
3207
|
+
process.stderr.write(`[CLI] org public key pinned to .governance.json (${pinned.slice(0, 8)}...).\n`);
|
|
3208
|
+
}
|
|
3209
|
+
else {
|
|
3210
|
+
process.stderr.write('[CLI] WARNING: could not fetch org public key for pinning. ' +
|
|
3211
|
+
'`ai-governance sync` will fail closed until the key is pinned. ' +
|
|
3212
|
+
'Re-run `ai-governance init` when the server is reachable to pin the key.\n');
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
2888
3215
|
}
|
|
2889
3216
|
else if (!agentDirFlag) {
|
|
2890
3217
|
interactiveInit(projectDir, { vendorCodeFlag, offlineFlag, licenseServerUrl, runtime: runtimeConfig }).catch(err => {
|
|
@@ -2987,6 +3314,19 @@ if (isDirectRun) {
|
|
|
2987
3314
|
});
|
|
2988
3315
|
});
|
|
2989
3316
|
}
|
|
3317
|
+
else if (command === 'sync') {
|
|
3318
|
+
// TS-010: pull server governance bundle and re-materialize local .governance.json.
|
|
3319
|
+
// Safe by default — no --apply = dry-run (shows diff, writes nothing).
|
|
3320
|
+
// See src/cli/sync.ts.
|
|
3321
|
+
import('./sync.js').then(({ runSyncCommand }) => {
|
|
3322
|
+
runSyncCommand(args.slice(1), projectDir).then((result) => {
|
|
3323
|
+
process.exit(result.errors > 0 ? 1 : 0);
|
|
3324
|
+
}).catch((e) => {
|
|
3325
|
+
process.stderr.write(`Error: ${e.message}\n`);
|
|
3326
|
+
process.exit(2);
|
|
3327
|
+
});
|
|
3328
|
+
});
|
|
3329
|
+
}
|
|
2990
3330
|
else if (command === 'scan') {
|
|
2991
3331
|
// A-2 (2026-05-19): customer-side OWASP Agentic Top 10 scan. Replaces
|
|
2992
3332
|
// the deprecated W-03 server-side Git-clone flow. Source code stays on
|