@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/cli/index.js
CHANGED
|
@@ -64,6 +64,7 @@ exports.writeCursorGovernanceRules = writeCursorGovernanceRules;
|
|
|
64
64
|
exports.writeContinueConfig = writeContinueConfig;
|
|
65
65
|
exports.writeGovernanceQuickstart = writeGovernanceQuickstart;
|
|
66
66
|
exports.writeGovernanceMcpSuggest = writeGovernanceMcpSuggest;
|
|
67
|
+
exports.writePerAgentIdentities = writePerAgentIdentities;
|
|
67
68
|
exports.nonInteractiveInit = nonInteractiveInit;
|
|
68
69
|
exports.retentionSweep = retentionSweep;
|
|
69
70
|
exports.verifyChain = verifyChain;
|
|
@@ -79,6 +80,8 @@ const governed_agent_1 = require("../governed-agent");
|
|
|
79
80
|
const agent_dir_scanner_1 = require("./agent-dir-scanner");
|
|
80
81
|
const normalize_agent_dir_1 = require("./normalize-agent-dir");
|
|
81
82
|
const wrap_shim_generator_1 = require("./wrap-shim-generator");
|
|
83
|
+
// Identity RFC B1 (per-folder identity files) — Pass-1 wiring (init --link).
|
|
84
|
+
const per_folder_identity_1 = require("./per-folder-identity");
|
|
82
85
|
const preflight_1 = require("./preflight");
|
|
83
86
|
const preflight_report_1 = require("./preflight-report");
|
|
84
87
|
/**
|
|
@@ -1264,6 +1267,25 @@ async function interactiveInit(projectDir, opts = {}) {
|
|
|
1264
1267
|
writeVendorCodeToConfig(projectDir, '', true);
|
|
1265
1268
|
process.stdout.write('Offline mode recorded in .governance.json\n');
|
|
1266
1269
|
}
|
|
1270
|
+
// TS-010 KEY-TRUST: pin the org public key from the server into
|
|
1271
|
+
// .governance.json immediately after the config is written.
|
|
1272
|
+
// The key is fetched over authenticated TLS from the same gov-server that
|
|
1273
|
+
// issued the serviceToken. Subsequent `ai-governance sync` calls verify
|
|
1274
|
+
// bundle signatures against this pinned key (never the bundle-embedded key).
|
|
1275
|
+
// Non-fatal: advisory if unavailable (Invariant 2), but sync will fail
|
|
1276
|
+
// closed (bundle rejected) until the key is pinned.
|
|
1277
|
+
if (opts.runtime?.govServerUrl && opts.runtime?.orgId) {
|
|
1278
|
+
const { fetchAndPinOrgPublicKey } = require('./sync.js');
|
|
1279
|
+
const pinned = fetchAndPinOrgPublicKey(opts.runtime.govServerUrl, opts.runtime.orgId, configPath);
|
|
1280
|
+
if (pinned) {
|
|
1281
|
+
process.stdout.write(`Org public key pinned for offline bundle verification.\n`);
|
|
1282
|
+
}
|
|
1283
|
+
else {
|
|
1284
|
+
process.stderr.write('WARNING: could not fetch org public key for pinning. ' +
|
|
1285
|
+
'`ai-governance sync` will reject bundles until the key is pinned. ' +
|
|
1286
|
+
'Re-run `ai-governance init` when the server is reachable.\n');
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1267
1289
|
// Install hooks
|
|
1268
1290
|
const { installed, errors } = installHooks(projectDir, config);
|
|
1269
1291
|
process.stdout.write(`Hooks installed: ${installed}\n`);
|
|
@@ -1745,6 +1767,109 @@ function writeGovernanceMcpSuggest(projectDir, tenantId) {
|
|
|
1745
1767
|
].join('\n'));
|
|
1746
1768
|
return path.relative(projectDir, mcpSuggestPath);
|
|
1747
1769
|
}
|
|
1770
|
+
/**
|
|
1771
|
+
* TS-002 CLI half — consume the registered[] list returned by register-fleet
|
|
1772
|
+
* and write a per-agent identity block into the project's .governance.json.
|
|
1773
|
+
*
|
|
1774
|
+
* Shape written:
|
|
1775
|
+
* ```json
|
|
1776
|
+
* {
|
|
1777
|
+
* "agents": [
|
|
1778
|
+
* { "localId": "auto-...", "agentId": "srv-uuid", "passportId": "pp-...",
|
|
1779
|
+
* "serviceToken": "eyJ...", "filePath": "src/agents/billing.py" }
|
|
1780
|
+
* ],
|
|
1781
|
+
* "runtime": {
|
|
1782
|
+
* "agentId": "<first-registered-agentId>",
|
|
1783
|
+
* "serviceToken": "<first-registered-serviceToken>",
|
|
1784
|
+
* ...rest of existing runtime block
|
|
1785
|
+
* }
|
|
1786
|
+
* }
|
|
1787
|
+
* ```
|
|
1788
|
+
*
|
|
1789
|
+
* Back-compat guarantee: runtime.agentId + runtime.serviceToken are updated to
|
|
1790
|
+
* the first registered agent so the existing audit-logger.sh → /cluster-events
|
|
1791
|
+
* push path (which reads these two flat fields) continues to work for the
|
|
1792
|
+
* single-agent case. The per-agent `agents[]` array is the source of truth for
|
|
1793
|
+
* multi-agent deployments; future per-agent attribution reads from there.
|
|
1794
|
+
*
|
|
1795
|
+
* Null-passport and null-serviceToken entries are written without throwing
|
|
1796
|
+
* (Invariant 2 — advisory, never blocking). A null serviceToken from agent N
|
|
1797
|
+
* does NOT overwrite the runtime.serviceToken that was already set.
|
|
1798
|
+
*
|
|
1799
|
+
* Idempotent: calling twice with the same input upserts by localId — does not
|
|
1800
|
+
* duplicate entries.
|
|
1801
|
+
*
|
|
1802
|
+
* @param projectDir Absolute path to the directory containing .governance.json
|
|
1803
|
+
* @param registered Array from register-fleet response.registered[]
|
|
1804
|
+
* @param localAgents Locally scanned DetectedAgent list (used to resolve filePath by localId)
|
|
1805
|
+
*/
|
|
1806
|
+
function writePerAgentIdentities(projectDir, registered, localAgents, opts = {}) {
|
|
1807
|
+
if (registered.length === 0)
|
|
1808
|
+
return;
|
|
1809
|
+
const configPath = path.join(projectDir, '.governance.json');
|
|
1810
|
+
// Read existing config — must preserve all existing keys (packs, hooks, etc.)
|
|
1811
|
+
let config = {};
|
|
1812
|
+
if (fs.existsSync(configPath)) {
|
|
1813
|
+
try {
|
|
1814
|
+
config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
1815
|
+
}
|
|
1816
|
+
catch { /* fresh config — start empty */ }
|
|
1817
|
+
}
|
|
1818
|
+
// Build a quick-lookup: localId → filePath from the local scan.
|
|
1819
|
+
// For entries where localId is absent, fall back to agentId as key.
|
|
1820
|
+
const filePathByLocalId = new Map();
|
|
1821
|
+
for (const la of localAgents) {
|
|
1822
|
+
filePathByLocalId.set(la.agentId, la.filePath);
|
|
1823
|
+
}
|
|
1824
|
+
// Build the new per-agent entries.
|
|
1825
|
+
const newEntries = registered.map((r) => {
|
|
1826
|
+
const key = r.localId ?? r.agentId;
|
|
1827
|
+
return {
|
|
1828
|
+
localId: key,
|
|
1829
|
+
agentId: r.agentId,
|
|
1830
|
+
passportId: r.passportId,
|
|
1831
|
+
serviceToken: r.serviceToken,
|
|
1832
|
+
filePath: filePathByLocalId.get(key),
|
|
1833
|
+
};
|
|
1834
|
+
});
|
|
1835
|
+
// Upsert into existing agents[] (idempotent — replace by localId).
|
|
1836
|
+
const existingEntries = Array.isArray(config['agents'])
|
|
1837
|
+
? config['agents']
|
|
1838
|
+
: [];
|
|
1839
|
+
const merged = [...existingEntries];
|
|
1840
|
+
for (const entry of newEntries) {
|
|
1841
|
+
const idx = merged.findIndex((e) => e.localId === entry.localId);
|
|
1842
|
+
if (idx >= 0) {
|
|
1843
|
+
merged[idx] = entry;
|
|
1844
|
+
}
|
|
1845
|
+
else {
|
|
1846
|
+
merged.push(entry);
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
config['agents'] = merged;
|
|
1850
|
+
// Back-compat: update runtime.agentId + runtime.serviceToken to the first
|
|
1851
|
+
// registered agent so the audit-logger.sh single-agent push path keeps working.
|
|
1852
|
+
// Only update runtime.serviceToken if the first agent's token is non-null
|
|
1853
|
+
// (do not overwrite a valid existing token with null).
|
|
1854
|
+
const first = newEntries[0];
|
|
1855
|
+
if (first) {
|
|
1856
|
+
const runtime = config['runtime'] ?? {};
|
|
1857
|
+
runtime['agentId'] = first.agentId;
|
|
1858
|
+
if (first.serviceToken !== null) {
|
|
1859
|
+
runtime['serviceToken'] = first.serviceToken;
|
|
1860
|
+
}
|
|
1861
|
+
if (opts.agentDir) {
|
|
1862
|
+
runtime['agentDir'] = opts.agentDir;
|
|
1863
|
+
}
|
|
1864
|
+
config['runtime'] = runtime;
|
|
1865
|
+
}
|
|
1866
|
+
// Write atomically — mode 0o600 (service tokens are secrets, Invariant 10).
|
|
1867
|
+
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), { mode: 0o600 });
|
|
1868
|
+
try {
|
|
1869
|
+
fs.chmodSync(configPath, 0o600);
|
|
1870
|
+
}
|
|
1871
|
+
catch { /* best-effort on Windows */ }
|
|
1872
|
+
}
|
|
1748
1873
|
// --- Non-interactive init (CI mode) ---
|
|
1749
1874
|
async function nonInteractiveInit(projectDir, opts) {
|
|
1750
1875
|
const licenseKey = opts.license || 'free';
|
|
@@ -2493,6 +2618,10 @@ Usage:
|
|
|
2493
2618
|
[--archive-dir <dir>] Archive destination (else delete)
|
|
2494
2619
|
[--dry-run] Report without acting
|
|
2495
2620
|
[--json] Machine-readable output
|
|
2621
|
+
ai-governance sync Pull server governance bundle to local .governance.json
|
|
2622
|
+
[--apply] Write changes (default: dry-run, shows diff only)
|
|
2623
|
+
[--agent <id>] Sync a specific agent only
|
|
2624
|
+
[--gov-server-url <url>] Override gov server URL
|
|
2496
2625
|
ai-governance rotate-token Rotate the runtime service token
|
|
2497
2626
|
Reads .governance.json runtime.serviceToken,
|
|
2498
2627
|
requests a fresh token from the gov-server,
|
|
@@ -2818,7 +2947,74 @@ if (isDirectRun) {
|
|
|
2818
2947
|
if (runtimeConfig?.serviceToken && runtimeConfig?.govServerUrl && detectedAgents.length > 0) {
|
|
2819
2948
|
try {
|
|
2820
2949
|
const { spawnSync } = require('node:child_process');
|
|
2950
|
+
// TS-009: capture project identity so the server can link agents to
|
|
2951
|
+
// the customer's actual project for audit roll-up in the portal.
|
|
2952
|
+
//
|
|
2953
|
+
// projectId: deterministic hash of the agent-dir absolute path.
|
|
2954
|
+
// Uses SHA-256(resolvedAgentDir) → first 16 hex chars. Stable across
|
|
2955
|
+
// re-runs on the same machine for the same project. If the customer
|
|
2956
|
+
// moves the repo, a new projectId is generated (a new project entity).
|
|
2957
|
+
//
|
|
2958
|
+
// projectName: basename of resolvedAgentDir (human-readable). If git
|
|
2959
|
+
// is available and resolvedAgentDir is inside a repo, use the repo
|
|
2960
|
+
// name (basename of the top-level). Falls back to dir basename.
|
|
2961
|
+
//
|
|
2962
|
+
// origin: git remote 'origin' URL, read locally by the CLI from the
|
|
2963
|
+
// customer's repo. The client's CLI reads its own git remote — we
|
|
2964
|
+
// NEVER scan or poll their infrastructure (Invariant 10: push-receive).
|
|
2965
|
+
// Only sent when git is available and the remote is set; never fails.
|
|
2966
|
+
//
|
|
2967
|
+
// Advisory (Invariant 2): any failure in this block produces null
|
|
2968
|
+
// values — the fleet payload is always sent and registration never
|
|
2969
|
+
// fails because of a missing project block.
|
|
2970
|
+
const captureProjectIdentity = () => {
|
|
2971
|
+
let origin;
|
|
2972
|
+
let projectName = path.basename(resolvedAgentDir);
|
|
2973
|
+
let gitRootDir;
|
|
2974
|
+
// Attempt to read git remote origin and repo name (client reads
|
|
2975
|
+
// its own git — Invariant 10 respected).
|
|
2976
|
+
try {
|
|
2977
|
+
const gitRemote = spawnSync('git', ['-C', resolvedAgentDir, 'remote', 'get-url', 'origin'], { encoding: 'utf8', timeout: 3000 });
|
|
2978
|
+
if (gitRemote.status === 0 && gitRemote.stdout) {
|
|
2979
|
+
origin = gitRemote.stdout.trim();
|
|
2980
|
+
// Derive projectName from the repo name embedded in the origin URL.
|
|
2981
|
+
// Works for both SSH (git@github.com:org/repo.git) and HTTPS
|
|
2982
|
+
// (https://github.com/org/repo.git). Strip trailing .git.
|
|
2983
|
+
const match = origin.match(/[\/:]([\w.-]+?)(?:\.git)?$/);
|
|
2984
|
+
if (match && match[1]) {
|
|
2985
|
+
projectName = match[1];
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
catch { /* git not available or not a git repo — use dir basename */ }
|
|
2990
|
+
try {
|
|
2991
|
+
// Best-effort: get the git top-level for a more stable projectId
|
|
2992
|
+
const gitRoot = spawnSync('git', ['-C', resolvedAgentDir, 'rev-parse', '--show-toplevel'], { encoding: 'utf8', timeout: 3000 });
|
|
2993
|
+
if (gitRoot.status === 0 && gitRoot.stdout) {
|
|
2994
|
+
gitRootDir = gitRoot.stdout.trim();
|
|
2995
|
+
// Override projectName with repo root basename when git is available
|
|
2996
|
+
const rootBasename = path.basename(gitRootDir);
|
|
2997
|
+
if (rootBasename)
|
|
2998
|
+
projectName = rootBasename;
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
catch { /* best-effort */ }
|
|
3002
|
+
// projectId: deterministic hash of the absolute project root dir
|
|
3003
|
+
// (git root when available, otherwise resolvedAgentDir). This gives
|
|
3004
|
+
// all register-fleet runs from the same repo the same projectId.
|
|
3005
|
+
const hashInput = gitRootDir ?? resolvedAgentDir;
|
|
3006
|
+
const projectId = 'proj-' + crypto.createHash('sha256').update(hashInput).digest('hex').slice(0, 16);
|
|
3007
|
+
return { projectId, projectName, ...(origin ? { origin } : {}) };
|
|
3008
|
+
};
|
|
3009
|
+
let projectIdentity;
|
|
3010
|
+
try {
|
|
3011
|
+
projectIdentity = captureProjectIdentity();
|
|
3012
|
+
}
|
|
3013
|
+
catch {
|
|
3014
|
+
// Advisory: project identity capture failure must never block fleet registration.
|
|
3015
|
+
}
|
|
2821
3016
|
const fleetPayload = {
|
|
3017
|
+
...(projectIdentity ? { project: projectIdentity } : {}),
|
|
2822
3018
|
agents: detectedAgents.map((a) => ({
|
|
2823
3019
|
localId: a.agentId,
|
|
2824
3020
|
name: a.name ?? undefined,
|
|
@@ -2830,6 +3026,17 @@ if (isDirectRun) {
|
|
|
2830
3026
|
// so the platform's OWASP/ASI analysis grades real tool surface.
|
|
2831
3027
|
tools: a.tools && a.tools.length > 0 ? a.tools : undefined,
|
|
2832
3028
|
mcpServers: a.mcpServers && a.mcpServers.length > 0 ? a.mcpServers : undefined,
|
|
3029
|
+
// TS-008 lossless capture (ticket A2): the scanner already
|
|
3030
|
+
// parses these from manifest.json; sending them closes the
|
|
3031
|
+
// model/version/vertical/allowed_domains/toolDetails/
|
|
3032
|
+
// max_concurrent_actions = null gap on the registered agent
|
|
3033
|
+
// (server validates + persists them — FleetAgentInput).
|
|
3034
|
+
model_identifier: a.model_identifier,
|
|
3035
|
+
version: a.version,
|
|
3036
|
+
vertical: a.vertical,
|
|
3037
|
+
allowed_domains: a.allowed_domains && a.allowed_domains.length > 0 ? a.allowed_domains : undefined,
|
|
3038
|
+
toolDetails: a.toolDetails && a.toolDetails.length > 0 ? a.toolDetails : undefined,
|
|
3039
|
+
max_concurrent_actions: a.max_concurrent_actions,
|
|
2833
3040
|
})),
|
|
2834
3041
|
};
|
|
2835
3042
|
const fleetResult = spawnSync('curl', [
|
|
@@ -2848,9 +3055,109 @@ if (isDirectRun) {
|
|
|
2848
3055
|
const resp = JSON.parse(fleetResult.stdout);
|
|
2849
3056
|
if (typeof resp.registeredCount === 'number') {
|
|
2850
3057
|
fleetRegistered = true;
|
|
2851
|
-
process.stdout.write(`\n✅
|
|
3058
|
+
process.stdout.write(`\n✅ Detected ${resp.registeredCount} agent(s) on the platform` +
|
|
2852
3059
|
(resp.skippedCount ? ` (${resp.skippedCount} already registered, skipped)` : '') +
|
|
2853
|
-
` — check your dashboard.\n`
|
|
3060
|
+
` — check your dashboard.\n` +
|
|
3061
|
+
// Slice 3c/3d (Decision 4): passports + runtime tokens are issued
|
|
3062
|
+
// at payment activation, not at detection. Tell the customer that
|
|
3063
|
+
// runtime push is unavailable until they Confirm + pay, and how to
|
|
3064
|
+
// activate it afterward.
|
|
3065
|
+
` ⏳ Runtime governance push is UNAVAILABLE until you Confirm + pay in the dashboard\n` +
|
|
3066
|
+
` (detected agents are not yet activated). After you Confirm, run\n` +
|
|
3067
|
+
` \`ai-governance sync --apply\` to activate each agent's runtime push.\n`);
|
|
3068
|
+
// TS-002 CLI half: persist per-agent identities into .governance.json.
|
|
3069
|
+
// Each agent gets its own serviceToken (sub=agent:<agentId>) so future
|
|
3070
|
+
// per-agent audit pushes carry the correct identity. The runtime block
|
|
3071
|
+
// is updated to the first agent for single-agent backward-compat.
|
|
3072
|
+
// Non-fatal (Invariant 2): if this write fails governance still runs.
|
|
3073
|
+
// Ghost-fix (#1717) fail-loud: print WHY any agent failed to
|
|
3074
|
+
// register — a bare count hid the b60d6cbc-class failures.
|
|
3075
|
+
if (Array.isArray(resp.failures) && resp.failures.length > 0) {
|
|
3076
|
+
for (const f of resp.failures) {
|
|
3077
|
+
process.stderr.write(` ❌ ${f.name}: registration failed — ${f.reason}\n`);
|
|
3078
|
+
}
|
|
3079
|
+
process.stderr.write(' Re-run the install command to retry the failed agent(s).\n');
|
|
3080
|
+
}
|
|
3081
|
+
if (typeof resp.healedGhostCount === 'number' && resp.healedGhostCount > 0) {
|
|
3082
|
+
process.stdout.write(` ♻ Healed ${resp.healedGhostCount} previously-stuck agent registration(s).\n`);
|
|
3083
|
+
}
|
|
3084
|
+
if (Array.isArray(resp.registered) && resp.registered.length > 0) {
|
|
3085
|
+
try {
|
|
3086
|
+
// Shield F1 (#1720, MEDIUM): server agentIds are BAKED into
|
|
3087
|
+
// executable shim files via raw template interpolation and
|
|
3088
|
+
// into URL paths — gate them against the canonical format
|
|
3089
|
+
// at the trust boundary before ANY downstream use. A
|
|
3090
|
+
// non-matching id from a compromised/MITM'd response is
|
|
3091
|
+
// dropped with a visible WARN, never interpolated.
|
|
3092
|
+
const SAFE_SERVER_AGENT_ID = /^agent-[0-9a-f]{8}$/;
|
|
3093
|
+
const safeRegistered = resp.registered.filter((r) => {
|
|
3094
|
+
if (SAFE_SERVER_AGENT_ID.test(r.agentId))
|
|
3095
|
+
return true;
|
|
3096
|
+
process.stderr.write(` [WARN] server returned a non-canonical agentId (${JSON.stringify(r.agentId).slice(0, 60)}) — entry dropped.\n`);
|
|
3097
|
+
return false;
|
|
3098
|
+
});
|
|
3099
|
+
const localRefs = detectedAgents.map((a) => ({
|
|
3100
|
+
agentId: a.agentId,
|
|
3101
|
+
filePath: a.filePath,
|
|
3102
|
+
}));
|
|
3103
|
+
// Persist the scan root so sync passes can resolve the
|
|
3104
|
+
// SCAN-ROOT-relative agents[].filePath to real folders.
|
|
3105
|
+
const agentDirRel = path.relative(projectDir, resolvedAgentDir) || '.';
|
|
3106
|
+
writePerAgentIdentities(projectDir, safeRegistered, localRefs, {
|
|
3107
|
+
agentDir: path.isAbsolute(agentDirRel) ? resolvedAgentDir : agentDirRel,
|
|
3108
|
+
});
|
|
3109
|
+
process.stdout.write(`[CLI] per-agent identities written to .governance.json ` +
|
|
3110
|
+
`(${safeRegistered.length} agent(s)).\n`);
|
|
3111
|
+
// Identity RFC B1 (Pass 1): land each agent's identity in
|
|
3112
|
+
// ITS OWN folder — <agent>/.connexum/identity.json. No
|
|
3113
|
+
// secret at this pass (tokens are minted at payment
|
|
3114
|
+
// activation), so this writes metadata regardless of git
|
|
3115
|
+
// state. Folder root = the SCAN root, not projectDir.
|
|
3116
|
+
const folderInputs = (0, per_folder_identity_1.buildPerFolderIdentityInputs)(safeRegistered, localRefs, {
|
|
3117
|
+
installationId: resp.installationId,
|
|
3118
|
+
signingKeyId: resp.signingKeyId,
|
|
3119
|
+
govServerUrl: resp.govServerUrl ?? runtimeConfig.govServerUrl,
|
|
3120
|
+
orgId: resp.orgId ?? runtimeConfig.orgId,
|
|
3121
|
+
}, new Date().toISOString());
|
|
3122
|
+
const folderRes = (0, per_folder_identity_1.writePerFolderIdentities)(resolvedAgentDir, folderInputs, {
|
|
3123
|
+
warn: (m) => process.stderr.write(m + '\n'),
|
|
3124
|
+
});
|
|
3125
|
+
process.stdout.write(`[CLI] per-folder identity files: ${folderRes.written.length} written` +
|
|
3126
|
+
(folderRes.skipped.length > 0 ? `, ${folderRes.skipped.length} skipped` : '') + '.\n');
|
|
3127
|
+
// Identity RFC C1 (Option B): regenerate each shim with
|
|
3128
|
+
// the SERVER agentId. Without this every shim keeps the
|
|
3129
|
+
// baked local auto-* id and all runtime signals collapse
|
|
3130
|
+
// onto ids the platform never issued. Pristine shims are
|
|
3131
|
+
// rebaked; edited shims get a VISIBLE warn (never a
|
|
3132
|
+
// silent skip — Stern BLOCK-2).
|
|
3133
|
+
const serverIdByLocalId = new Map();
|
|
3134
|
+
for (const r of safeRegistered) {
|
|
3135
|
+
if (r.localId)
|
|
3136
|
+
serverIdByLocalId.set(r.localId, r.agentId);
|
|
3137
|
+
}
|
|
3138
|
+
const regenResults = (0, wrap_shim_generator_1.regenerateShimAgentIds)(detectedAgents, serverIdByLocalId, resolvedAgentDir, (msg) => process.stdout.write(msg + '\n'));
|
|
3139
|
+
const rebaked = regenResults.filter((r) => r.status === 'regenerated' || r.status === 'verified').length;
|
|
3140
|
+
const stuck = regenResults.filter((r) => r.status === 'skipped-edited' || r.status === 'failed');
|
|
3141
|
+
process.stdout.write(`[CLI] shim identity rebake: ${rebaked} on server ids` +
|
|
3142
|
+
(stuck.length > 0 ? `, ${stuck.length} NEED ATTENTION (listed above)` : '') + '.\n');
|
|
3143
|
+
if (stuck.length > 0) {
|
|
3144
|
+
// Stern F1 (#1720): the recovery path must be in the
|
|
3145
|
+
// operator-facing summary, not buried in per-file WARNs.
|
|
3146
|
+
process.stderr.write('⚠️ The agent(s) listed above still emit their LOCAL id at runtime — their\n' +
|
|
3147
|
+
' signals will not attach to the registered platform identity until fixed.\n' +
|
|
3148
|
+
' Recovery: if you did NOT edit the .governed file yourself, DELETE it and\n' +
|
|
3149
|
+
' re-run this install command — it will be regenerated with the correct id.\n' +
|
|
3150
|
+
' If you did edit it, set its AGENT_ID to the server id shown above.\n');
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
catch (identityErr) {
|
|
3154
|
+
// Non-fatal — fleet is registered on the platform; local identity
|
|
3155
|
+
// write failure must not undo that success (Invariant 2).
|
|
3156
|
+
process.stderr.write(`[CLI] per-agent identity write failed (non-fatal): ` +
|
|
3157
|
+
`${identityErr.message}. ` +
|
|
3158
|
+
`Agents are registered on the platform; re-run init to retry the write.\n`);
|
|
3159
|
+
}
|
|
3160
|
+
}
|
|
2854
3161
|
}
|
|
2855
3162
|
else if (resp.error) {
|
|
2856
3163
|
process.stderr.write(`[CLI] register-fleet: server error: ${resp.error}\n`);
|
|
@@ -2941,6 +3248,27 @@ if (isDirectRun) {
|
|
|
2941
3248
|
runtime: runtimeConfig,
|
|
2942
3249
|
agentDir: agentDirArg,
|
|
2943
3250
|
});
|
|
3251
|
+
// TS-010 KEY-TRUST: pin the org public key from the server into
|
|
3252
|
+
// .governance.json immediately after init writes the config file.
|
|
3253
|
+
// This is the trusted pinning event — the key is fetched over the same
|
|
3254
|
+
// authenticated TLS channel that issued the runtimeConfig exchange.
|
|
3255
|
+
// Subsequent `ai-governance sync` calls verify bundle signatures against
|
|
3256
|
+
// this pinned key. Non-fatal: advisory if unavailable (Invariant 2), but
|
|
3257
|
+
// sync will fail closed until the key is pinned.
|
|
3258
|
+
if (runtimeConfig?.govServerUrl && runtimeConfig?.orgId) {
|
|
3259
|
+
// fetchAndPinOrgPublicKey is imported from sync.ts
|
|
3260
|
+
const { fetchAndPinOrgPublicKey } = require('./sync.js');
|
|
3261
|
+
const configPath = path.join(projectDir, '.governance.json');
|
|
3262
|
+
const pinned = fetchAndPinOrgPublicKey(runtimeConfig.govServerUrl, runtimeConfig.orgId, configPath);
|
|
3263
|
+
if (pinned) {
|
|
3264
|
+
process.stderr.write(`[CLI] org public key pinned to .governance.json (${pinned.slice(0, 8)}...).\n`);
|
|
3265
|
+
}
|
|
3266
|
+
else {
|
|
3267
|
+
process.stderr.write('[CLI] WARNING: could not fetch org public key for pinning. ' +
|
|
3268
|
+
'`ai-governance sync` will fail closed until the key is pinned. ' +
|
|
3269
|
+
'Re-run `ai-governance init` when the server is reachable to pin the key.\n');
|
|
3270
|
+
}
|
|
3271
|
+
}
|
|
2944
3272
|
}
|
|
2945
3273
|
else if (!agentDirFlag) {
|
|
2946
3274
|
interactiveInit(projectDir, { vendorCodeFlag, offlineFlag, licenseServerUrl, runtime: runtimeConfig }).catch(err => {
|
|
@@ -3043,6 +3371,19 @@ if (isDirectRun) {
|
|
|
3043
3371
|
});
|
|
3044
3372
|
});
|
|
3045
3373
|
}
|
|
3374
|
+
else if (command === 'sync') {
|
|
3375
|
+
// TS-010: pull server governance bundle and re-materialize local .governance.json.
|
|
3376
|
+
// Safe by default — no --apply = dry-run (shows diff, writes nothing).
|
|
3377
|
+
// See src/cli/sync.ts.
|
|
3378
|
+
Promise.resolve().then(() => __importStar(require('./sync.js'))).then(({ runSyncCommand }) => {
|
|
3379
|
+
runSyncCommand(args.slice(1), projectDir).then((result) => {
|
|
3380
|
+
process.exit(result.errors > 0 ? 1 : 0);
|
|
3381
|
+
}).catch((e) => {
|
|
3382
|
+
process.stderr.write(`Error: ${e.message}\n`);
|
|
3383
|
+
process.exit(2);
|
|
3384
|
+
});
|
|
3385
|
+
});
|
|
3386
|
+
}
|
|
3046
3387
|
else if (command === 'scan') {
|
|
3047
3388
|
// A-2 (2026-05-19): customer-side OWASP Agentic Top 10 scan. Replaces
|
|
3048
3389
|
// the deprecated W-03 server-side Git-clone flow. Source code stays on
|