@intutic/cli 1.9.0 → 1.10.1
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/README.md +1 -0
- package/dist/cli.js +868 -30
- package/dist/cli.js.map +1 -1
- package/dist/commands/attenuate.d.ts +39 -0
- package/dist/commands/attenuate.d.ts.map +1 -0
- package/dist/commands/attenuate.js +98 -0
- package/dist/commands/attenuate.js.map +1 -0
- package/dist/commands/budget.d.ts +80 -0
- package/dist/commands/budget.d.ts.map +1 -1
- package/dist/commands/budget.js +119 -3
- package/dist/commands/budget.js.map +1 -1
- package/dist/commands/connect.d.ts +10 -0
- package/dist/commands/connect.d.ts.map +1 -1
- package/dist/commands/connect.js +358 -48
- package/dist/commands/connect.js.map +1 -1
- package/dist/commands/credentials.d.ts +31 -0
- package/dist/commands/credentials.d.ts.map +1 -0
- package/dist/commands/credentials.js +134 -0
- package/dist/commands/credentials.js.map +1 -0
- package/dist/commands/decision.d.ts +37 -0
- package/dist/commands/decision.d.ts.map +1 -0
- package/dist/commands/decision.js +56 -0
- package/dist/commands/decision.js.map +1 -0
- package/dist/commands/doctor.d.ts +38 -1
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +228 -18
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/enforce.d.ts +33 -0
- package/dist/commands/enforce.d.ts.map +1 -0
- package/dist/commands/enforce.js +150 -0
- package/dist/commands/enforce.js.map +1 -0
- package/dist/commands/enterprise.d.ts +29 -0
- package/dist/commands/enterprise.d.ts.map +1 -0
- package/dist/commands/enterprise.js +141 -0
- package/dist/commands/enterprise.js.map +1 -0
- package/dist/commands/env.d.ts +7 -0
- package/dist/commands/env.d.ts.map +1 -0
- package/dist/commands/env.js +55 -0
- package/dist/commands/env.js.map +1 -0
- package/dist/commands/exec.d.ts +38 -2
- package/dist/commands/exec.d.ts.map +1 -1
- package/dist/commands/exec.js +247 -10
- package/dist/commands/exec.js.map +1 -1
- package/dist/commands/findings.d.ts +84 -0
- package/dist/commands/findings.d.ts.map +1 -0
- package/dist/commands/findings.js +329 -0
- package/dist/commands/findings.js.map +1 -0
- package/dist/commands/gateway.d.ts +62 -0
- package/dist/commands/gateway.d.ts.map +1 -0
- package/dist/commands/gateway.js +298 -0
- package/dist/commands/gateway.js.map +1 -0
- package/dist/commands/guardrails.d.ts +81 -0
- package/dist/commands/guardrails.d.ts.map +1 -0
- package/dist/commands/guardrails.js +735 -0
- package/dist/commands/guardrails.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +14 -2
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/install-daemon.d.ts +70 -3
- package/dist/commands/install-daemon.d.ts.map +1 -1
- package/dist/commands/install-daemon.js +482 -47
- package/dist/commands/install-daemon.js.map +1 -1
- package/dist/commands/integrity.d.ts +205 -0
- package/dist/commands/integrity.d.ts.map +1 -0
- package/dist/commands/integrity.js +620 -0
- package/dist/commands/integrity.js.map +1 -0
- package/dist/commands/judge.d.ts +31 -0
- package/dist/commands/judge.d.ts.map +1 -0
- package/dist/commands/judge.js +118 -0
- package/dist/commands/judge.js.map +1 -0
- package/dist/commands/org.d.ts +31 -0
- package/dist/commands/org.d.ts.map +1 -0
- package/dist/commands/org.js +159 -0
- package/dist/commands/org.js.map +1 -0
- package/dist/commands/policy.d.ts +138 -4
- package/dist/commands/policy.d.ts.map +1 -1
- package/dist/commands/policy.js +538 -97
- package/dist/commands/policy.js.map +1 -1
- package/dist/commands/predict-cost.d.ts +45 -0
- package/dist/commands/predict-cost.d.ts.map +1 -0
- package/dist/commands/predict-cost.js +123 -0
- package/dist/commands/predict-cost.js.map +1 -0
- package/dist/commands/rollback.d.ts +5 -0
- package/dist/commands/rollback.d.ts.map +1 -0
- package/dist/commands/rollback.js +142 -0
- package/dist/commands/rollback.js.map +1 -0
- package/dist/commands/routing.d.ts +62 -0
- package/dist/commands/routing.d.ts.map +1 -0
- package/dist/commands/routing.js +121 -0
- package/dist/commands/routing.js.map +1 -0
- package/dist/commands/setup.d.ts +62 -0
- package/dist/commands/setup.d.ts.map +1 -0
- package/dist/commands/setup.js +238 -0
- package/dist/commands/setup.js.map +1 -0
- package/dist/commands/skill.d.ts +268 -1
- package/dist/commands/skill.d.ts.map +1 -1
- package/dist/commands/skill.js +744 -39
- package/dist/commands/skill.js.map +1 -1
- package/dist/commands/sops.d.ts +95 -0
- package/dist/commands/sops.d.ts.map +1 -1
- package/dist/commands/sops.js +334 -34
- package/dist/commands/sops.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +1 -18
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/status.d.ts.map +1 -1
- package/dist/commands/status.js +44 -1
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/team.d.ts +38 -0
- package/dist/commands/team.d.ts.map +1 -0
- package/dist/commands/team.js +140 -0
- package/dist/commands/team.js.map +1 -0
- package/dist/commands/traces.d.ts +8 -1
- package/dist/commands/traces.d.ts.map +1 -1
- package/dist/commands/traces.js +114 -15
- package/dist/commands/traces.js.map +1 -1
- package/dist/commands/whoami.d.ts.map +1 -1
- package/dist/commands/whoami.js +2 -1
- package/dist/commands/whoami.js.map +1 -1
- package/dist/config/keychain.d.ts +10 -2
- package/dist/config/keychain.d.ts.map +1 -1
- package/dist/config/keychain.js +83 -14
- package/dist/config/keychain.js.map +1 -1
- package/dist/config/paths.d.ts +27 -0
- package/dist/config/paths.d.ts.map +1 -1
- package/dist/config/paths.js +37 -6
- package/dist/config/paths.js.map +1 -1
- package/dist/config/store.d.ts.map +1 -1
- package/dist/config/store.js +19 -3
- package/dist/config/store.js.map +1 -1
- package/dist/harness/ag2.d.ts +22 -0
- package/dist/harness/ag2.d.ts.map +1 -0
- package/dist/harness/ag2.js +34 -0
- package/dist/harness/ag2.js.map +1 -0
- package/dist/harness/agentcore.d.ts +55 -0
- package/dist/harness/agentcore.d.ts.map +1 -0
- package/dist/harness/agentcore.js +119 -0
- package/dist/harness/agentcore.js.map +1 -0
- package/dist/harness/agenticOrchestrator.d.ts +52 -0
- package/dist/harness/agenticOrchestrator.d.ts.map +1 -0
- package/dist/harness/agenticOrchestrator.js +115 -0
- package/dist/harness/agenticOrchestrator.js.map +1 -0
- package/dist/harness/aiSdkHarness.d.ts +30 -0
- package/dist/harness/aiSdkHarness.d.ts.map +1 -0
- package/dist/harness/aiSdkHarness.js +51 -0
- package/dist/harness/aiSdkHarness.js.map +1 -0
- package/dist/harness/aiSdkWorkflow.d.ts +26 -0
- package/dist/harness/aiSdkWorkflow.d.ts.map +1 -0
- package/dist/harness/aiSdkWorkflow.js +39 -0
- package/dist/harness/aiSdkWorkflow.js.map +1 -0
- package/dist/harness/autogen.d.ts +24 -0
- package/dist/harness/autogen.d.ts.map +1 -0
- package/dist/harness/autogen.js +36 -0
- package/dist/harness/autogen.js.map +1 -0
- package/dist/harness/base.js.map +1 -1
- package/dist/harness/continue.d.ts.map +1 -1
- package/dist/harness/continue.js +2 -1
- package/dist/harness/continue.js.map +1 -1
- package/dist/harness/crewai.d.ts +15 -0
- package/dist/harness/crewai.d.ts.map +1 -0
- package/dist/harness/crewai.js +26 -0
- package/dist/harness/crewai.js.map +1 -0
- package/dist/harness/detector.d.ts +1 -1
- package/dist/harness/detector.d.ts.map +1 -1
- package/dist/harness/detector.js +47 -1
- package/dist/harness/detector.js.map +1 -1
- package/dist/harness/dsh.d.ts +19 -0
- package/dist/harness/dsh.d.ts.map +1 -0
- package/dist/harness/dsh.js +93 -0
- package/dist/harness/dsh.js.map +1 -0
- package/dist/harness/eve.d.ts +40 -0
- package/dist/harness/eve.d.ts.map +1 -0
- package/dist/harness/eve.js +67 -0
- package/dist/harness/eve.js.map +1 -0
- package/dist/harness/googleAdk.d.ts +18 -0
- package/dist/harness/googleAdk.d.ts.map +1 -0
- package/dist/harness/googleAdk.js +29 -0
- package/dist/harness/googleAdk.js.map +1 -0
- package/dist/harness/goose.d.ts.map +1 -1
- package/dist/harness/goose.js +5 -1
- package/dist/harness/goose.js.map +1 -1
- package/dist/harness/grok.d.ts +16 -0
- package/dist/harness/grok.d.ts.map +1 -0
- package/dist/harness/grok.js +78 -0
- package/dist/harness/grok.js.map +1 -0
- package/dist/harness/jsSdkGatedAdapter.d.ts +102 -0
- package/dist/harness/jsSdkGatedAdapter.d.ts.map +1 -0
- package/dist/harness/jsSdkGatedAdapter.js +139 -0
- package/dist/harness/jsSdkGatedAdapter.js.map +1 -0
- package/dist/harness/langchain.d.ts +22 -0
- package/dist/harness/langchain.d.ts.map +1 -0
- package/dist/harness/langchain.js +33 -0
- package/dist/harness/langchain.js.map +1 -0
- package/dist/harness/langgraph.d.ts +22 -0
- package/dist/harness/langgraph.d.ts.map +1 -0
- package/dist/harness/langgraph.js +80 -0
- package/dist/harness/langgraph.js.map +1 -0
- package/dist/harness/mastra.d.ts +21 -0
- package/dist/harness/mastra.d.ts.map +1 -0
- package/dist/harness/mastra.js +33 -0
- package/dist/harness/mastra.js.map +1 -0
- package/dist/harness/muse.d.ts +22 -0
- package/dist/harness/muse.d.ts.map +1 -0
- package/dist/harness/muse.js +92 -0
- package/dist/harness/muse.js.map +1 -0
- package/dist/harness/n8n.d.ts.map +1 -1
- package/dist/harness/n8n.js +36 -11
- package/dist/harness/n8n.js.map +1 -1
- package/dist/harness/openaiAgents.d.ts +40 -0
- package/dist/harness/openaiAgents.d.ts.map +1 -0
- package/dist/harness/openaiAgents.js +107 -0
- package/dist/harness/openaiAgents.js.map +1 -0
- package/dist/harness/opencode.d.ts +24 -0
- package/dist/harness/opencode.d.ts.map +1 -0
- package/dist/harness/opencode.js +97 -0
- package/dist/harness/opencode.js.map +1 -0
- package/dist/harness/pydanticAi.d.ts +19 -0
- package/dist/harness/pydanticAi.d.ts.map +1 -0
- package/dist/harness/pydanticAi.js +30 -0
- package/dist/harness/pydanticAi.js.map +1 -0
- package/dist/harness/sdkGatedAdapter.d.ts +55 -0
- package/dist/harness/sdkGatedAdapter.d.ts.map +1 -0
- package/dist/harness/sdkGatedAdapter.js +85 -0
- package/dist/harness/sdkGatedAdapter.js.map +1 -0
- package/dist/harness/smolagents.d.ts +22 -0
- package/dist/harness/smolagents.d.ts.map +1 -0
- package/dist/harness/smolagents.js +33 -0
- package/dist/harness/smolagents.js.map +1 -0
- package/dist/harness/strands.d.ts +22 -0
- package/dist/harness/strands.d.ts.map +1 -0
- package/dist/harness/strands.js +35 -0
- package/dist/harness/strands.js.map +1 -0
- package/dist/harness/trueforge.d.ts +25 -0
- package/dist/harness/trueforge.d.ts.map +1 -0
- package/dist/harness/trueforge.js +40 -0
- package/dist/harness/trueforge.js.map +1 -0
- package/dist/harness/types.d.ts.map +1 -1
- package/dist/harness/types.js +70 -0
- package/dist/harness/types.js.map +1 -1
- package/dist/harness/vercelAiSdk.d.ts +31 -0
- package/dist/harness/vercelAiSdk.d.ts.map +1 -0
- package/dist/harness/vercelAiSdk.js +44 -0
- package/dist/harness/vercelAiSdk.js.map +1 -0
- package/dist/harness/xirp.d.ts +43 -0
- package/dist/harness/xirp.d.ts.map +1 -0
- package/dist/harness/xirp.js +95 -0
- package/dist/harness/xirp.js.map +1 -0
- package/dist/lib/api.d.ts +68 -0
- package/dist/lib/api.d.ts.map +1 -1
- package/dist/lib/api.js +108 -67
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/authMessages.d.ts +19 -0
- package/dist/lib/authMessages.d.ts.map +1 -0
- package/dist/lib/authMessages.js +20 -0
- package/dist/lib/authMessages.js.map +1 -0
- package/dist/lib/binaryChecksum.d.ts +12 -0
- package/dist/lib/binaryChecksum.d.ts.map +1 -0
- package/dist/lib/binaryChecksum.js +45 -0
- package/dist/lib/binaryChecksum.js.map +1 -0
- package/dist/lib/binaryOnPath.d.ts +24 -0
- package/dist/lib/binaryOnPath.d.ts.map +1 -0
- package/dist/lib/binaryOnPath.js +31 -0
- package/dist/lib/binaryOnPath.js.map +1 -0
- package/dist/lib/caTrust.d.ts +69 -0
- package/dist/lib/caTrust.d.ts.map +1 -0
- package/dist/lib/caTrust.js +169 -0
- package/dist/lib/caTrust.js.map +1 -0
- package/dist/lib/ciscoScanner.d.ts +178 -0
- package/dist/lib/ciscoScanner.d.ts.map +1 -0
- package/dist/lib/ciscoScanner.js +292 -0
- package/dist/lib/ciscoScanner.js.map +1 -0
- package/dist/lib/decisionsLogRefresh.d.ts +28 -0
- package/dist/lib/decisionsLogRefresh.d.ts.map +1 -0
- package/dist/lib/decisionsLogRefresh.js +54 -0
- package/dist/lib/decisionsLogRefresh.js.map +1 -0
- package/dist/lib/deviceReport.d.ts +26 -0
- package/dist/lib/deviceReport.d.ts.map +1 -0
- package/dist/lib/deviceReport.js +55 -0
- package/dist/lib/deviceReport.js.map +1 -0
- package/dist/lib/elevation.d.ts +58 -0
- package/dist/lib/elevation.d.ts.map +1 -0
- package/dist/lib/elevation.js +83 -0
- package/dist/lib/elevation.js.map +1 -0
- package/dist/lib/enforcementState.d.ts +69 -0
- package/dist/lib/enforcementState.d.ts.map +1 -0
- package/dist/lib/enforcementState.js +113 -0
- package/dist/lib/enforcementState.js.map +1 -0
- package/dist/lib/ensureValkey.d.ts.map +1 -1
- package/dist/lib/ensureValkey.js +2 -2
- package/dist/lib/ensureValkey.js.map +1 -1
- package/dist/lib/envInjector.d.ts.map +1 -1
- package/dist/lib/envInjector.js +22 -4
- package/dist/lib/envInjector.js.map +1 -1
- package/dist/lib/gitHooks.d.ts +7 -2
- package/dist/lib/gitHooks.d.ts.map +1 -1
- package/dist/lib/gitHooks.js +180 -13
- package/dist/lib/gitHooks.js.map +1 -1
- package/dist/lib/gracefulShutdown.d.ts +37 -0
- package/dist/lib/gracefulShutdown.d.ts.map +1 -0
- package/dist/lib/gracefulShutdown.js +57 -0
- package/dist/lib/gracefulShutdown.js.map +1 -0
- package/dist/lib/graphIdentity.d.ts +80 -0
- package/dist/lib/graphIdentity.d.ts.map +1 -0
- package/dist/lib/graphIdentity.js +120 -0
- package/dist/lib/graphIdentity.js.map +1 -0
- package/dist/lib/localTraces.d.ts +61 -0
- package/dist/lib/localTraces.d.ts.map +1 -0
- package/dist/lib/localTraces.js +173 -0
- package/dist/lib/localTraces.js.map +1 -0
- package/dist/lib/mdmManifest.d.ts +73 -0
- package/dist/lib/mdmManifest.d.ts.map +1 -0
- package/dist/lib/mdmManifest.js +190 -0
- package/dist/lib/mdmManifest.js.map +1 -0
- package/dist/lib/onboarding.d.ts.map +1 -1
- package/dist/lib/onboarding.js +60 -1
- package/dist/lib/onboarding.js.map +1 -1
- package/dist/lib/otelPropagation.d.ts +26 -0
- package/dist/lib/otelPropagation.d.ts.map +1 -0
- package/dist/lib/otelPropagation.js +40 -0
- package/dist/lib/otelPropagation.js.map +1 -0
- package/dist/lib/policySnapshot.d.ts +92 -0
- package/dist/lib/policySnapshot.d.ts.map +1 -0
- package/dist/lib/policySnapshot.js +173 -0
- package/dist/lib/policySnapshot.js.map +1 -0
- package/dist/lib/process.d.ts.map +1 -1
- package/dist/lib/process.js +60 -0
- package/dist/lib/process.js.map +1 -1
- package/dist/lib/providerProbe.d.ts +28 -0
- package/dist/lib/providerProbe.d.ts.map +1 -0
- package/dist/lib/providerProbe.js +57 -0
- package/dist/lib/providerProbe.js.map +1 -0
- package/dist/lib/proxyBinary.d.ts +19 -0
- package/dist/lib/proxyBinary.d.ts.map +1 -0
- package/dist/lib/proxyBinary.js +30 -0
- package/dist/lib/proxyBinary.js.map +1 -0
- package/dist/lib/sandbox/firecracker.d.ts +89 -0
- package/dist/lib/sandbox/firecracker.d.ts.map +1 -0
- package/dist/lib/sandbox/firecracker.js +135 -0
- package/dist/lib/sandbox/firecracker.js.map +1 -0
- package/dist/lib/sandbox/index.d.ts +34 -0
- package/dist/lib/sandbox/index.d.ts.map +1 -0
- package/dist/lib/sandbox/index.js +54 -0
- package/dist/lib/sandbox/index.js.map +1 -0
- package/dist/lib/sandbox/oci.d.ts +38 -0
- package/dist/lib/sandbox/oci.d.ts.map +1 -0
- package/dist/lib/sandbox/oci.js +108 -0
- package/dist/lib/sandbox/oci.js.map +1 -0
- package/dist/lib/sandbox/types.d.ts +48 -0
- package/dist/lib/sandbox/types.d.ts.map +1 -0
- package/dist/lib/sandbox/types.js +12 -0
- package/dist/lib/sandbox/types.js.map +1 -0
- package/dist/lib/sopFrontMatter.d.ts +50 -0
- package/dist/lib/sopFrontMatter.d.ts.map +1 -0
- package/dist/lib/sopFrontMatter.js +129 -0
- package/dist/lib/sopFrontMatter.js.map +1 -0
- package/package.json +11 -6
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `intutic guardrails` — the Policy Clause Ledger from the terminal
|
|
3
|
+
* (LLD #71, Wave 6): sources, documents, the review queue, and the three
|
|
4
|
+
* decisions that move a cited guardrail. A client of
|
|
5
|
+
* `/api/v1/policy-guardrails/*` and `/api/v1/connectors`; nothing here
|
|
6
|
+
* decides anything the server would not.
|
|
7
|
+
*
|
|
8
|
+
* Not `intutic policy` (the WASM rule loop) and not `intutic sops` (the
|
|
9
|
+
* file plane). The acting identity is never a flag: the server records the
|
|
10
|
+
* authenticated member, so a client-supplied name cannot forge the audit
|
|
11
|
+
* trail — same rule as `intutic findings adjudicate`.
|
|
12
|
+
*/
|
|
13
|
+
import { readFile, writeFile, mkdir, readdir, unlink } from 'node:fs/promises';
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { log } from '../lib/logger.js';
|
|
17
|
+
import { parseSopFile, withContentHash } from '../lib/sopFrontMatter.js';
|
|
18
|
+
import { NOT_AUTHENTICATED } from '../lib/authMessages.js';
|
|
19
|
+
import { loadCredentials, loadConfig } from '../config/store.js';
|
|
20
|
+
import { resolveControlPlaneUrl } from '../config/paths.js';
|
|
21
|
+
import { createApiClient } from '../lib/api.js';
|
|
22
|
+
import { SOURCE_PROVIDERS, GUARDRAIL_STATUSES, GUARDRAIL_TARGETS, guardrailIdFromSopTitle, guardrailFileStem, } from '@intutic/shared-types';
|
|
23
|
+
const BASE = '/api/v1/policy-guardrails';
|
|
24
|
+
function errMessage(err) {
|
|
25
|
+
return err instanceof Error ? err.message : String(err);
|
|
26
|
+
}
|
|
27
|
+
async function getClient(dev) {
|
|
28
|
+
const creds = await loadCredentials();
|
|
29
|
+
if (!creds) {
|
|
30
|
+
log.error(NOT_AUTHENTICATED);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
const config = loadConfig();
|
|
34
|
+
const devMode = dev || config?.devMode || process.env.INTUTIC_DEV === '1';
|
|
35
|
+
return createApiClient(resolveControlPlaneUrl(devMode), creds.apiKey);
|
|
36
|
+
}
|
|
37
|
+
function emitJson(value) {
|
|
38
|
+
console.log(JSON.stringify(value, null, 2));
|
|
39
|
+
}
|
|
40
|
+
/** A null rate is "no measured rate" — never invented as 0%. */
|
|
41
|
+
function formatRate(rate) {
|
|
42
|
+
return rate === null ? 'no measured rate' : `${(rate * 100).toFixed(1)}%`;
|
|
43
|
+
}
|
|
44
|
+
const enc = encodeURIComponent;
|
|
45
|
+
export async function runGuardrailsSourcesList(opts) {
|
|
46
|
+
const client = await getClient(opts.dev);
|
|
47
|
+
let items;
|
|
48
|
+
try {
|
|
49
|
+
const res = await client.get('/api/v1/connectors');
|
|
50
|
+
items = (res.items ?? []).filter((c) => SOURCE_PROVIDERS.includes(c.provider));
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
log.error(`Failed to list policy sources: ${errMessage(err)}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
if (opts.json) {
|
|
57
|
+
emitJson(items);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
log.header('Intutic — Policy Sources');
|
|
61
|
+
if (items.length === 0) {
|
|
62
|
+
log.info(`No policy sources. Add one with \`intutic guardrails sources add <${SOURCE_PROVIDERS.join('|')}> --name … --token …\`.`);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
for (const c of items) {
|
|
66
|
+
const id = c.connector_id ?? c.connectorId ?? '?';
|
|
67
|
+
const synced = c.last_synced_at ?? c.lastSyncedAt ?? null;
|
|
68
|
+
const cfg = c.config ?? {};
|
|
69
|
+
log.field(c.name, `${id} — ${c.provider}${cfg.auto_sync === true ? ', auto-sync' : ''}${synced ? `, last synced ${synced}` : ', never synced'}`);
|
|
70
|
+
}
|
|
71
|
+
log.info(`${items.length} policy source(s).`);
|
|
72
|
+
}
|
|
73
|
+
export async function runGuardrailsSourcesAdd(provider, opts) {
|
|
74
|
+
if (!SOURCE_PROVIDERS.includes(provider)) {
|
|
75
|
+
log.error(`Provider must be one of ${SOURCE_PROVIDERS.join(', ')}.`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
if (!opts.name) {
|
|
79
|
+
log.error('--name is required.');
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
if (Boolean(opts.token) === Boolean(opts.tokenFile)) {
|
|
83
|
+
log.error('Give the credential with exactly one of --token or --token-file (a Google service-account key is a file).');
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
let credential = opts.token ?? '';
|
|
87
|
+
if (opts.tokenFile) {
|
|
88
|
+
try {
|
|
89
|
+
credential = (await readFile(opts.tokenFile, 'utf8')).trim();
|
|
90
|
+
}
|
|
91
|
+
catch (err) {
|
|
92
|
+
log.error(`Could not read --token-file: ${errMessage(err)}`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
if (!credential) {
|
|
96
|
+
log.error('--token-file is empty.');
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
let config = {};
|
|
101
|
+
if (opts.config) {
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(opts.config);
|
|
104
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
105
|
+
throw new Error('not an object');
|
|
106
|
+
config = parsed;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
log.error(`--config must be a JSON object: ${errMessage(err)}`);
|
|
110
|
+
process.exit(1);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const client = await getClient(opts.dev);
|
|
114
|
+
try {
|
|
115
|
+
const res = await client.post('/api/v1/connectors', {
|
|
116
|
+
provider,
|
|
117
|
+
name: opts.name,
|
|
118
|
+
token: credential,
|
|
119
|
+
config,
|
|
120
|
+
});
|
|
121
|
+
const id = res.connector?.connector_id ?? res.connector?.connectorId ?? res.connectorId ?? res.connector_id ?? '(see dashboard)';
|
|
122
|
+
if (opts.json) {
|
|
123
|
+
emitJson(res);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
log.success(`Added ${provider} source "${opts.name}" (${id}). Sync it with \`intutic guardrails sources sync ${id}\`.`);
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
log.error(`Failed to add source: ${errMessage(err)}`);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
export async function runGuardrailsSourcesSync(connectorId, opts) {
|
|
134
|
+
const client = await getClient(opts.dev);
|
|
135
|
+
try {
|
|
136
|
+
const res = await client.post(`/api/v1/connectors/${enc(connectorId)}/sync`, {});
|
|
137
|
+
if (opts.json) {
|
|
138
|
+
emitJson(res);
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
log.success(`Synced ${connectorId}.`);
|
|
142
|
+
log.field('Documents processed', String(res.processedCount ?? 0));
|
|
143
|
+
log.field('Documents changed', String(res.changedDocIds?.length ?? 0));
|
|
144
|
+
log.field('SOPs written', String(res.updatedSopIds?.length ?? 0));
|
|
145
|
+
if (res.successorSopIds?.length)
|
|
146
|
+
log.field('Upstream successors', String(res.successorSopIds.length));
|
|
147
|
+
if (res.upstreamDeleted?.length)
|
|
148
|
+
log.field('Upstream deleted', res.upstreamDeleted.join(', '));
|
|
149
|
+
if (res.staleGuardrailIds?.length)
|
|
150
|
+
log.warn(`${res.staleGuardrailIds.length} guardrail citation(s) went stale: ${res.staleGuardrailIds.join(', ')}`);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
log.error(`Failed to sync source: ${errMessage(err)}`);
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
// ─── docs ───────────────────────────────────────────────────────────
|
|
158
|
+
export async function runGuardrailsDocsList(opts) {
|
|
159
|
+
const client = await getClient(opts.dev);
|
|
160
|
+
let documents;
|
|
161
|
+
try {
|
|
162
|
+
documents = (await client.get(`${BASE}/documents`)).documents ?? [];
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
log.error(`Failed to list documents: ${errMessage(err)}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
if (opts.json) {
|
|
169
|
+
emitJson(documents);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
log.header('Intutic — Policy Documents');
|
|
173
|
+
if (documents.length === 0) {
|
|
174
|
+
log.info('No documents yet. Sync a source first.');
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
for (const d of documents) {
|
|
178
|
+
log.field(d.title, `${d.docId} — ${d.provider}, ${d.passageCount} passage(s), ${d.clauseCount} clause(s), ${d.guardrailCount} guardrail(s)${d.injectionFlagged ? ', INJECTION-FLAGGED' : ''}${d.lastRun ? `, last extraction ${d.lastRun.extractor} at ${d.lastRun.startedAt}${d.lastRun.error ? ` (error: ${d.lastRun.error})` : ''}` : ', never extracted'}`);
|
|
179
|
+
}
|
|
180
|
+
log.info(`${documents.length} document(s).`);
|
|
181
|
+
}
|
|
182
|
+
export async function runGuardrailsDocsShow(docId, opts) {
|
|
183
|
+
const client = await getClient(opts.dev);
|
|
184
|
+
const { status, body } = await client.getWithStatus(`${BASE}/documents/${enc(docId)}`);
|
|
185
|
+
if (status === 404) {
|
|
186
|
+
log.error(`Document "${docId}" not found in this workspace.`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
if (status !== 200 || !body.document) {
|
|
190
|
+
log.error(`Failed to load document (${status}): ${body.error ?? 'unknown error'}`);
|
|
191
|
+
process.exit(1);
|
|
192
|
+
}
|
|
193
|
+
const d = body.document;
|
|
194
|
+
if (opts.json) {
|
|
195
|
+
emitJson(d);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
log.header(`Intutic — ${d.title}`);
|
|
199
|
+
log.field('Document', d.docId);
|
|
200
|
+
log.field('Source', `${d.provider}${d.sourceUrl ? ` ${d.sourceUrl}` : ''}`);
|
|
201
|
+
log.field('Content', `${d.contentHash.slice(0, 12)}${d.upstreamVersion ? ` (upstream ${d.upstreamVersion})` : ''}`);
|
|
202
|
+
log.field('Passages', String(d.passages.length));
|
|
203
|
+
if (d.injectionFlagged)
|
|
204
|
+
log.warn('This document is flagged for prompt-injection phrasing; its guardrails are created REJECTED.');
|
|
205
|
+
if (d.clauses.length === 0)
|
|
206
|
+
log.info('No clauses extracted yet. Run `intutic guardrails docs extract <docId>`.');
|
|
207
|
+
for (const c of d.clauses) {
|
|
208
|
+
const refused = Array.isArray(c.validation) ? c.validation.filter((v) => !v.passed).map((v) => v.name) : [];
|
|
209
|
+
log.field(`${c.kind} [${c.status}]`, `${c.clauseId}${c.guardrailId ? ` → ${c.guardrailStatus} guardrail ${c.guardrailId}` : ''}${refused.length ? ` — refused by ${refused.join(', ')}` : ''}`);
|
|
210
|
+
log.dim(` "${c.quote}"`);
|
|
211
|
+
}
|
|
212
|
+
for (const r of d.runs) {
|
|
213
|
+
log.dim(` run ${r.runId}: ${r.extractor} — ${r.clausesValid} valid / ${r.clausesRejected} refused${r.error ? ` — ${r.error}` : ''}`);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
export async function runGuardrailsDocsExtract(docId, opts) {
|
|
217
|
+
const client = await getClient(opts.dev);
|
|
218
|
+
const { status, body } = await client.postWithStatus(`${BASE}/documents/${enc(docId)}/extract`, { llm: !opts.noLlm });
|
|
219
|
+
if (status === 404) {
|
|
220
|
+
log.error(`Document "${docId}" not found in this workspace.`);
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
if (status === 403) {
|
|
224
|
+
log.error(body.error ?? 'Extraction needs a paid plan or an active trial.');
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
if (status === 429) {
|
|
228
|
+
log.warn(`Daily extraction cap reached (${body.cap?.count ?? '?'} of ${body.cap?.cap ?? '?'} calls today). The front-matter lift still ran.`);
|
|
229
|
+
process.exit(1);
|
|
230
|
+
}
|
|
231
|
+
if (status !== 200 || !body.result) {
|
|
232
|
+
log.error(`Extraction failed (${status}): ${body.error ?? 'unknown error'}`);
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
const r = body.result;
|
|
236
|
+
if (opts.json) {
|
|
237
|
+
emitJson(r);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
if (r.skipped === 'llm_disabled')
|
|
241
|
+
log.info('Model call skipped (--no-llm); only the front-matter lift ran.');
|
|
242
|
+
else if (r.skipped === 'no_passages')
|
|
243
|
+
log.warn('The document has no passages to extract from.');
|
|
244
|
+
else if (r.skipped === 'cap_unavailable')
|
|
245
|
+
log.warn('The cap store was unreachable, so no model call was made.');
|
|
246
|
+
else if (r.llmUnavailable)
|
|
247
|
+
log.warn(`The model could not be reached: ${r.error ?? 'no detail'}. The run is recorded with its error.`);
|
|
248
|
+
else
|
|
249
|
+
log.success(`Extracted ${docId}: ${r.proposals} proposal(s) from ${r.chunks} chunk(s).`);
|
|
250
|
+
log.field('Valid clauses', String(r.valid));
|
|
251
|
+
log.field('Refused by a check', String(r.rejected));
|
|
252
|
+
log.field('Malformed', String(r.malformed));
|
|
253
|
+
log.field('Guardrails proposed', String(r.guardrails.proposed));
|
|
254
|
+
if (r.guardrails.rejectedForInjection > 0)
|
|
255
|
+
log.field('Rejected (injection-flagged document)', String(r.guardrails.rejectedForInjection));
|
|
256
|
+
if (r.guardrails.existing > 0)
|
|
257
|
+
log.field('Already existed', String(r.guardrails.existing));
|
|
258
|
+
if (r.lifted.clauses > 0)
|
|
259
|
+
log.field('Front-matter rules lifted', String(r.lifted.clauses));
|
|
260
|
+
if (r.runId)
|
|
261
|
+
log.dim(` run ${r.runId}`);
|
|
262
|
+
}
|
|
263
|
+
// ─── search ─────────────────────────────────────────────────────────
|
|
264
|
+
export async function runGuardrailsSearch(token, opts) {
|
|
265
|
+
if (opts.text)
|
|
266
|
+
return runPassageTextSearch(token, opts);
|
|
267
|
+
if (!token.trim()) {
|
|
268
|
+
log.error('A tool name or action token is required, e.g. `intutic guardrails search bash`.');
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
const client = await getClient(opts.dev);
|
|
272
|
+
let coverage;
|
|
273
|
+
try {
|
|
274
|
+
coverage = (await client.get(`${BASE}/coverage?token=${enc(token.trim())}`)).coverage;
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
log.error(`Search failed: ${errMessage(err)}`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
if (opts.json) {
|
|
281
|
+
emitJson(coverage);
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
log.header(`Intutic — Passages mentioning "${coverage.token}"`);
|
|
285
|
+
if (coverage.passages.length === 0) {
|
|
286
|
+
log.info('No passage mentions it. A guardrail naming it would have nothing to cite.');
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
for (const p of coverage.passages) {
|
|
290
|
+
log.field(p.title, `${p.headingPath.join(' › ') || '(no heading)'} — ${p.excerpt}`);
|
|
291
|
+
}
|
|
292
|
+
log.info(`${coverage.passages.length} passage(s); ${coverage.guardrails.length} guardrail(s) stand on them.`);
|
|
293
|
+
for (const g of coverage.guardrails)
|
|
294
|
+
log.dim(` ${g.guardrailId} [${g.status}] ${g.target} — "${g.quote}"`);
|
|
295
|
+
}
|
|
296
|
+
/** `search --text`: the words of every live passage, stemmed, best match first. */
|
|
297
|
+
async function runPassageTextSearch(query, opts) {
|
|
298
|
+
if (!query.trim()) {
|
|
299
|
+
log.error('Words to search for are required, e.g. `intutic guardrails search --text "production deploy"`.');
|
|
300
|
+
process.exit(1);
|
|
301
|
+
}
|
|
302
|
+
const client = await getClient(opts.dev);
|
|
303
|
+
let result;
|
|
304
|
+
try {
|
|
305
|
+
result = (await client.get(`${BASE}/search?q=${enc(query.trim())}`)).search;
|
|
306
|
+
}
|
|
307
|
+
catch (err) {
|
|
308
|
+
log.error(`Search failed: ${errMessage(err)}`);
|
|
309
|
+
process.exit(1);
|
|
310
|
+
}
|
|
311
|
+
if (opts.json) {
|
|
312
|
+
emitJson(result);
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
log.header(`Intutic — Passages about "${result.query}"`);
|
|
316
|
+
if (result.passages.length === 0) {
|
|
317
|
+
log.info('No live passage matches those words.');
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
for (const p of result.passages) {
|
|
321
|
+
log.field(p.title, `${p.headingPath.join(' › ') || '(no heading)'} — ${p.excerpt}`);
|
|
322
|
+
}
|
|
323
|
+
log.info(`${result.passages.length} passage(s), best match first.`);
|
|
324
|
+
}
|
|
325
|
+
// ─── impact / duplicates ────────────────────────────────────────────
|
|
326
|
+
export async function runGuardrailsImpact(opts) {
|
|
327
|
+
if (!opts.doc === !opts.passage) {
|
|
328
|
+
log.error('Name exactly one of --doc <docId> or --passage <passageId>.');
|
|
329
|
+
process.exit(1);
|
|
330
|
+
}
|
|
331
|
+
const client = await getClient(opts.dev);
|
|
332
|
+
const seed = opts.doc ? `docId=${enc(opts.doc)}` : `passageId=${enc(opts.passage)}`;
|
|
333
|
+
let impact;
|
|
334
|
+
try {
|
|
335
|
+
impact = (await client.get(`${BASE}/impact?${seed}`)).impact;
|
|
336
|
+
}
|
|
337
|
+
catch (err) {
|
|
338
|
+
log.error(`Impact failed: ${errMessage(err)}`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
if (opts.json) {
|
|
342
|
+
emitJson(impact);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
log.header(`Intutic — What a change to ${opts.doc ?? opts.passage} reaches`);
|
|
346
|
+
log.field('Passages', `${impact.passages.length} within ${impact.maxDepth} computed edge(s)${impact.truncated ? ' (capped)' : ''}`);
|
|
347
|
+
log.field('Clauses citing them', String(impact.clauses.length));
|
|
348
|
+
log.field('Guardrails', String(impact.guardrails.length));
|
|
349
|
+
for (const g of impact.guardrails) {
|
|
350
|
+
log.dim(` ${g.guardrailId} [${g.status}${g.sourceStale ? ', stale' : ''}] ${g.target}, ${g.depth} edge(s) from the change`);
|
|
351
|
+
}
|
|
352
|
+
if (impact.guardrails.some((g) => g.status === 'ENFORCING')) {
|
|
353
|
+
log.warn('An enforcing guardrail stands on this text. An upstream edit marks it stale for review; it never switches enforcement off.');
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
export async function runGuardrailsDuplicates(opts) {
|
|
357
|
+
let query = '';
|
|
358
|
+
if (opts.minJaccard !== undefined) {
|
|
359
|
+
const n = Number(opts.minJaccard);
|
|
360
|
+
if (opts.minJaccard.trim() === '' || !Number.isFinite(n) || n < 0 || n > 1) {
|
|
361
|
+
log.error('--min-jaccard must be a number between 0 and 1.');
|
|
362
|
+
process.exit(1);
|
|
363
|
+
}
|
|
364
|
+
query = `?minJaccard=${n}`;
|
|
365
|
+
}
|
|
366
|
+
const client = await getClient(opts.dev);
|
|
367
|
+
let d;
|
|
368
|
+
try {
|
|
369
|
+
d = (await client.get(`${BASE}/duplicates${query}`)).duplicates;
|
|
370
|
+
}
|
|
371
|
+
catch (err) {
|
|
372
|
+
log.error(`Failed to list duplicates: ${errMessage(err)}`);
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
if (opts.json) {
|
|
376
|
+
emitJson(d);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
log.header('Intutic — Duplicate passages and repeated rules');
|
|
380
|
+
if (d.passagePairs.length === 0 && d.sameRule.length === 0) {
|
|
381
|
+
log.info(`No passages overlap at Jaccard ${d.minJaccard.toFixed(2)} or above, and no rule is cited twice.`);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
for (const pp of d.passagePairs) {
|
|
385
|
+
log.field(`Jaccard ${pp.jaccard.toFixed(2)}${pp.nearIdentical ? ', near-identical' : ''}`, `${pp.a.title} and ${pp.b.title} (${pp.intersection} of ${pp.union} shingles shared)`);
|
|
386
|
+
log.dim(` ${pp.a.passageId}: ${pp.a.excerpt}`);
|
|
387
|
+
log.dim(` ${pp.b.passageId}: ${pp.b.excerpt}`);
|
|
388
|
+
}
|
|
389
|
+
for (const r of d.sameRule) {
|
|
390
|
+
log.field(`The same ${r.kind} rule`, `cited ${r.clauses.length} times`);
|
|
391
|
+
for (const c of r.clauses)
|
|
392
|
+
log.dim(` ${c.clauseId} in ${c.title}${c.guardrailId ? ` (${c.guardrailId}, ${c.guardrailStatus})` : ''}: "${c.quote}"`);
|
|
393
|
+
}
|
|
394
|
+
log.info(`${d.passagePairs.length} overlapping pair(s); ${d.sameRule.length} repeated rule(s).`);
|
|
395
|
+
}
|
|
396
|
+
// ─── guardrails ─────────────────────────────────────────────────────
|
|
397
|
+
export async function runGuardrailsList(opts) {
|
|
398
|
+
const params = new URLSearchParams();
|
|
399
|
+
if (opts.status !== undefined) {
|
|
400
|
+
if (!GUARDRAIL_STATUSES.includes(opts.status)) {
|
|
401
|
+
log.error(`--status must be one of ${GUARDRAIL_STATUSES.join(', ')}.`);
|
|
402
|
+
process.exit(1);
|
|
403
|
+
}
|
|
404
|
+
params.set('status', opts.status);
|
|
405
|
+
}
|
|
406
|
+
if (opts.target !== undefined) {
|
|
407
|
+
if (!GUARDRAIL_TARGETS.includes(opts.target)) {
|
|
408
|
+
log.error(`--target must be one of ${GUARDRAIL_TARGETS.join(', ')}.`);
|
|
409
|
+
process.exit(1);
|
|
410
|
+
}
|
|
411
|
+
params.set('target', opts.target);
|
|
412
|
+
}
|
|
413
|
+
if (opts.doc)
|
|
414
|
+
params.set('docId', opts.doc);
|
|
415
|
+
if (opts.limit !== undefined) {
|
|
416
|
+
const n = Number(opts.limit);
|
|
417
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
418
|
+
log.error('--limit must be a positive integer.');
|
|
419
|
+
process.exit(1);
|
|
420
|
+
}
|
|
421
|
+
params.set('limit', String(n));
|
|
422
|
+
}
|
|
423
|
+
const client = await getClient(opts.dev);
|
|
424
|
+
let guardrails;
|
|
425
|
+
try {
|
|
426
|
+
const q = params.toString();
|
|
427
|
+
guardrails = (await client.get(`${BASE}/guardrails${q ? `?${q}` : ''}`)).guardrails ?? [];
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
log.error(`Failed to list guardrails: ${errMessage(err)}`);
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
if (opts.json) {
|
|
434
|
+
emitJson(guardrails);
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
log.header('Intutic — Policy Guardrails');
|
|
438
|
+
if (guardrails.length === 0) {
|
|
439
|
+
log.info(`No guardrails${opts.status ? ` with status ${opts.status}` : ''}.`);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
for (const g of guardrails) {
|
|
443
|
+
const ir = g.ir;
|
|
444
|
+
const title = ir.title ?? ir.kind;
|
|
445
|
+
log.field(`${g.guardrailId} [${g.status}${g.sourceStale ? ', stale' : ''}]`, `${g.target} — ${title}`);
|
|
446
|
+
log.dim(` "${g.clause.quote}" — ${g.document.title}${g.document.sourceUrl ? ` ${g.document.sourceUrl}` : ''}`);
|
|
447
|
+
if (g.status === 'SHADOW')
|
|
448
|
+
log.dim(` shadow: ${g.shadowEvaluations} evaluation(s), ${g.shadowWouldAct} would-act`);
|
|
449
|
+
}
|
|
450
|
+
log.info(`${guardrails.length} guardrail(s).`);
|
|
451
|
+
}
|
|
452
|
+
function printReadiness(r) {
|
|
453
|
+
log.field('Ready to enforce', r.ready ? (r.neverFired ? 'yes — but no observed traffic exercised it; promote with --acknowledge-no-traffic' : 'yes') : 'no');
|
|
454
|
+
log.field('Shadow evaluations', `${r.evaluations}/${r.thresholds.minShadowEvaluations}`);
|
|
455
|
+
log.field('Would-act rate', `${formatRate(r.wouldActRate)} (ceiling ${(r.thresholds.maxWouldActRate * 100).toFixed(0)}%)`);
|
|
456
|
+
log.field('Adjudicated fires', `${r.adjudicated}/${r.adjudicatedRequired}, FP rate ${formatRate(r.falsePositiveRate)} (ceiling ${(r.thresholds.maxAdjudicatedFalsePositiveRate * 100).toFixed(0)}%)`);
|
|
457
|
+
for (const reason of r.reasons)
|
|
458
|
+
log.dim(` ${reason}`);
|
|
459
|
+
}
|
|
460
|
+
export async function runGuardrailsShow(guardrailId, opts) {
|
|
461
|
+
const client = await getClient(opts.dev);
|
|
462
|
+
const { status, body } = await client.getWithStatus(`${BASE}/guardrails/${enc(guardrailId)}`);
|
|
463
|
+
if (status === 404) {
|
|
464
|
+
log.error(`Guardrail "${guardrailId}" not found in this workspace.`);
|
|
465
|
+
process.exit(1);
|
|
466
|
+
}
|
|
467
|
+
if (status !== 200 || !body.guardrail) {
|
|
468
|
+
log.error(`Failed to load guardrail (${status}): ${body.error ?? 'unknown error'}`);
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
const g = body.guardrail;
|
|
472
|
+
let readiness = null;
|
|
473
|
+
if (g.status === 'SHADOW') {
|
|
474
|
+
try {
|
|
475
|
+
readiness = (await client.get(`${BASE}/guardrails/${enc(guardrailId)}/readiness`)).readiness;
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
readiness = null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
if (opts.json) {
|
|
482
|
+
emitJson({ guardrail: g, readiness });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
const ir = g.ir;
|
|
486
|
+
log.header(`Intutic — ${ir.title ?? ir.kind}`);
|
|
487
|
+
log.field('Guardrail', `${g.guardrailId} [${g.status}${g.sourceStale ? ', stale citation' : ''}] ${g.target}`);
|
|
488
|
+
log.field('Cites', `"${g.clause.quote}"`);
|
|
489
|
+
log.field('From', `${g.document.title} (${g.document.provider})${g.document.sourceUrl ? ` ${g.document.sourceUrl}` : ''} — passage ${g.clause.passageHash.slice(0, 12)}`);
|
|
490
|
+
const rendered = g.rendered;
|
|
491
|
+
if (g.target === 'hook_rule' && rendered.toolPattern) {
|
|
492
|
+
log.field('Tool pattern', rendered.toolPattern);
|
|
493
|
+
if (rendered.argPattern)
|
|
494
|
+
log.field('Input pattern', rendered.argPattern);
|
|
495
|
+
log.field('On block', `[Intutic Governance] BLOCKED: ${rendered.reason ?? ''} [sop.guardrail.${g.guardrailId}]`);
|
|
496
|
+
}
|
|
497
|
+
else if (g.target === 'sop_front_matter' && rendered.lines) {
|
|
498
|
+
log.field('Front matter', '');
|
|
499
|
+
for (const line of rendered.lines.split('\n'))
|
|
500
|
+
log.dim(` ${line}`);
|
|
501
|
+
}
|
|
502
|
+
else if (g.target === 'wasm_rule' && rendered.source) {
|
|
503
|
+
log.field('Predicate source', '');
|
|
504
|
+
for (const line of rendered.source.split('\n'))
|
|
505
|
+
log.dim(` ${line}`);
|
|
506
|
+
}
|
|
507
|
+
if (g.status === 'REJECTED' && g.rejectedReason)
|
|
508
|
+
log.field('Rejected', g.rejectedReason);
|
|
509
|
+
if (readiness)
|
|
510
|
+
printReadiness(readiness);
|
|
511
|
+
const checks = Array.isArray(g.validation) ? g.validation : [];
|
|
512
|
+
if (checks.length > 0)
|
|
513
|
+
log.field('Checks', checks.map((c) => `${c.passed ? '✓' : '✗'} ${c.name}`).join(' '));
|
|
514
|
+
for (const e of g.events)
|
|
515
|
+
log.dim(` ${e.createdAt} ${e.event}${e.actorId ? ` by ${e.actorId}` : ' (system)'}`);
|
|
516
|
+
}
|
|
517
|
+
async function transition(guardrailId, action, body, opts, done) {
|
|
518
|
+
const client = await getClient(opts.dev);
|
|
519
|
+
const { status, body: res } = await client.postWithStatus(`${BASE}/guardrails/${enc(guardrailId)}/${action}`, body);
|
|
520
|
+
if (status === 404) {
|
|
521
|
+
log.error(`Guardrail "${guardrailId}" not found in this workspace.`);
|
|
522
|
+
process.exit(1);
|
|
523
|
+
}
|
|
524
|
+
if (status === 403) {
|
|
525
|
+
log.error(res.error ?? 'Refused: a guardrail transition needs a signed-in member of the workspace.');
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
if (status === 409) {
|
|
529
|
+
log.error(res.error ?? `Refused (${res.code ?? 'conflict'}).`);
|
|
530
|
+
if (res.readiness)
|
|
531
|
+
printReadiness(res.readiness);
|
|
532
|
+
process.exit(1);
|
|
533
|
+
}
|
|
534
|
+
if (status !== 200 || !res.guardrail) {
|
|
535
|
+
log.error(`${action} failed (${status}): ${res.error ?? 'unknown error'}`);
|
|
536
|
+
process.exit(1);
|
|
537
|
+
}
|
|
538
|
+
if (opts.json) {
|
|
539
|
+
emitJson(res);
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
log.success(`${guardrailId}: ${done}`);
|
|
543
|
+
if (res.readiness)
|
|
544
|
+
printReadiness(res.readiness);
|
|
545
|
+
if (action === 'approve-shadow' && res.guardrail.target === 'wasm_rule' && res.guardrail.ruleCandidateId) {
|
|
546
|
+
// A wasm guardrail is enforced by nothing this row projects: it was handed
|
|
547
|
+
// to the rule-candidate pipeline, and the next step is a compile.
|
|
548
|
+
log.info(`Handed off to rule candidate ${res.guardrail.ruleCandidateId}. Next: intutic policy compile --candidate ${res.guardrail.ruleCandidateId} --upload`);
|
|
549
|
+
}
|
|
550
|
+
const last = res.guardrail.events.at(-1);
|
|
551
|
+
if (last)
|
|
552
|
+
log.dim(` ${last.event}${last.actorId ? ` by ${last.actorId}` : ''}`);
|
|
553
|
+
}
|
|
554
|
+
export async function runGuardrailsApproveShadow(guardrailId, opts) {
|
|
555
|
+
await transition(guardrailId, 'approve-shadow', {}, opts, 'now in SHADOW — distributed as warn, measuring, enforcing nothing.');
|
|
556
|
+
}
|
|
557
|
+
export async function runGuardrailsPromote(guardrailId, opts) {
|
|
558
|
+
await transition(guardrailId, 'promote', { acknowledgeNoTraffic: opts.acknowledgeNoTraffic === true }, opts, 'now ENFORCING.');
|
|
559
|
+
}
|
|
560
|
+
export async function runGuardrailsReject(guardrailId, opts) {
|
|
561
|
+
if (!opts.reason || !opts.reason.trim()) {
|
|
562
|
+
log.error('--reason is required; the rejection is recorded with it.');
|
|
563
|
+
process.exit(1);
|
|
564
|
+
}
|
|
565
|
+
await transition(guardrailId, 'reject', { reason: opts.reason.trim() }, opts, 'rejected.');
|
|
566
|
+
}
|
|
567
|
+
export async function runGuardrailsRetire(guardrailId, opts) {
|
|
568
|
+
await transition(guardrailId, 'retire', {}, opts, 'retired — gone from every rule endpoint on the next poll.');
|
|
569
|
+
}
|
|
570
|
+
export async function runGuardrailsReconfirm(guardrailId, opts) {
|
|
571
|
+
await transition(guardrailId, 'reconfirm', {}, opts, 'citation re-confirmed against a live passage.');
|
|
572
|
+
}
|
|
573
|
+
export async function runGuardrailsReplay(guardrailId, opts) {
|
|
574
|
+
const client = await getClient(opts.dev);
|
|
575
|
+
const { status, body } = await client.postWithStatus(`${BASE}/guardrails/${enc(guardrailId)}/replay`, {});
|
|
576
|
+
if (status === 404) {
|
|
577
|
+
log.error(`Guardrail "${guardrailId}" not found in this workspace, or it has no replay (WASM candidates replay through \`intutic policy replay\`).`);
|
|
578
|
+
process.exit(1);
|
|
579
|
+
}
|
|
580
|
+
if (status !== 200 || !body.replay) {
|
|
581
|
+
log.error(`Replay failed (${status}): ${body.error ?? 'unknown error'}`);
|
|
582
|
+
process.exit(1);
|
|
583
|
+
}
|
|
584
|
+
const r = body.replay;
|
|
585
|
+
if (opts.json) {
|
|
586
|
+
emitJson(r);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
log.header(`Intutic — Replay ${guardrailId}`);
|
|
590
|
+
log.field('Would have fired', `${r.fires} of ${r.captured} captured call(s)`);
|
|
591
|
+
log.field('Source', `${r.source === 'enforcement_log' ? 'hook-gate and proxy verdicts with a captured input' : 'sampled request contexts'}, last ${r.windowDays} day(s)${r.truncated ? ', capped' : ''}`);
|
|
592
|
+
if (r.unsupported.length > 0)
|
|
593
|
+
log.warn(`Cannot be replayed: ${r.unsupported.join(', ')}`);
|
|
594
|
+
for (const s of r.sample)
|
|
595
|
+
log.dim(` ${s.at} ${s.toolName}: ${s.excerpt}`);
|
|
596
|
+
}
|
|
597
|
+
export async function runGuardrailsConflicts(opts) {
|
|
598
|
+
const client = await getClient(opts.dev);
|
|
599
|
+
let conflicts;
|
|
600
|
+
try {
|
|
601
|
+
conflicts = (await client.get(`${BASE}/conflicts`)).conflicts ?? [];
|
|
602
|
+
}
|
|
603
|
+
catch (err) {
|
|
604
|
+
log.error(`Failed to detect conflicts: ${errMessage(err)}`);
|
|
605
|
+
process.exit(1);
|
|
606
|
+
}
|
|
607
|
+
if (opts.json) {
|
|
608
|
+
emitJson(conflicts);
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
log.header('Intutic — Guardrail Conflicts');
|
|
612
|
+
if (conflicts.length === 0) {
|
|
613
|
+
log.info('No two live guardrails or lifted front-matter rules contradict each other.');
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
for (const c of conflicts) {
|
|
617
|
+
log.field(c.kind, `${c.token ?? ''} — ${c.detail}`);
|
|
618
|
+
log.dim(` ${c.a.id}: "${c.a.quote}"`);
|
|
619
|
+
log.dim(` ${c.b.id}: "${c.b.quote}"`);
|
|
620
|
+
}
|
|
621
|
+
log.info(`${conflicts.length} conflict(s).`);
|
|
622
|
+
}
|
|
623
|
+
const sha256 = (text) => createHash('sha256').update(text, 'utf8').digest('hex');
|
|
624
|
+
/**
|
|
625
|
+
* `intutic guardrails pull` — the SHADOW and ENFORCING front-matter
|
|
626
|
+
* guardrails `GET /api/v1/workspace/sops-policy` serves, written to
|
|
627
|
+
* `.intutic/sops/guardrail-<id>.md` for a proxy that reads SOPs from disk.
|
|
628
|
+
*
|
|
629
|
+
* Flat, not in a subdirectory: the proxy's loader reads one directory level
|
|
630
|
+
* and titles each SOP by its file stem, and `guardrail-<id>` is a stem the
|
|
631
|
+
* control plane credits shadow reports to. The served markdown already
|
|
632
|
+
* carries its own front-matter fence with the enforcing keys and
|
|
633
|
+
* `mode: shadow`; `withContentHash` puts the pull marker inside that fence
|
|
634
|
+
* rather than wrapping a second one the proxy would never read.
|
|
635
|
+
*
|
|
636
|
+
* Same overwrite rule as `intutic sops pull`: a file whose recorded hash no
|
|
637
|
+
* longer matches its body was edited by hand and is left alone without
|
|
638
|
+
* `--force`. A file for a guardrail that is no longer served (retired or
|
|
639
|
+
* rejected) is reported — a proxy reading the directory still enforces it —
|
|
640
|
+
* and removed only with `--prune`, and only when it is unmodified.
|
|
641
|
+
*/
|
|
642
|
+
export async function runGuardrailsPull(opts) {
|
|
643
|
+
const config = loadConfig();
|
|
644
|
+
const workspaceRoot = config?.workspaceRoot ?? process.cwd();
|
|
645
|
+
const sopsDir = join(workspaceRoot, '.intutic', 'sops');
|
|
646
|
+
const say = (level, message) => {
|
|
647
|
+
if (opts.json)
|
|
648
|
+
return;
|
|
649
|
+
if (level === 'warn')
|
|
650
|
+
log.warn(message);
|
|
651
|
+
else
|
|
652
|
+
log.info(message);
|
|
653
|
+
};
|
|
654
|
+
if (!opts.json)
|
|
655
|
+
log.header('Intutic — Pull Guardrails');
|
|
656
|
+
const client = await getClient(opts.dev);
|
|
657
|
+
let served;
|
|
658
|
+
try {
|
|
659
|
+
served = (await client.get('/api/v1/workspace/sops-policy')).sops ?? [];
|
|
660
|
+
}
|
|
661
|
+
catch (err) {
|
|
662
|
+
log.error(`Failed to fetch the workspace SOP policy: ${errMessage(err)}`);
|
|
663
|
+
process.exit(1);
|
|
664
|
+
}
|
|
665
|
+
const guardrails = served
|
|
666
|
+
.map((entry) => ({ id: guardrailIdFromSopTitle(entry.title), entry }))
|
|
667
|
+
.filter((x) => x.id !== null && typeof x.entry.markdownContent === 'string' && x.entry.markdownContent.trim().length > 0);
|
|
668
|
+
await mkdir(sopsDir, { recursive: true });
|
|
669
|
+
const written = [];
|
|
670
|
+
const unchanged = [];
|
|
671
|
+
const skipped = [];
|
|
672
|
+
const servedStems = new Set();
|
|
673
|
+
for (const { id, entry } of guardrails) {
|
|
674
|
+
const stem = guardrailFileStem(id);
|
|
675
|
+
servedStems.add(stem);
|
|
676
|
+
const filePath = join(sopsDir, `${stem}.md`);
|
|
677
|
+
const rendered = withContentHash(`${entry.markdownContent.trim()}\n`);
|
|
678
|
+
const existing = await readFile(filePath, 'utf-8').catch(() => null);
|
|
679
|
+
if (existing !== null) {
|
|
680
|
+
if (existing === rendered) {
|
|
681
|
+
unchanged.push(stem);
|
|
682
|
+
continue;
|
|
683
|
+
}
|
|
684
|
+
if (!opts.force) {
|
|
685
|
+
const parsedExisting = parseSopFile(existing);
|
|
686
|
+
const dirty = parsedExisting.contentHash === null || parsedExisting.contentHash !== sha256(parsedExisting.body);
|
|
687
|
+
if (dirty) {
|
|
688
|
+
say('warn', `${filePath}: locally modified (or not written by pull) — refusing to overwrite. Use --force to pull anyway.`);
|
|
689
|
+
skipped.push(stem);
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
await writeFile(filePath, rendered, 'utf-8');
|
|
695
|
+
written.push(stem);
|
|
696
|
+
say('info', `${filePath} ← ${entry.title}`);
|
|
697
|
+
}
|
|
698
|
+
// Files this command wrote for guardrails the server no longer serves.
|
|
699
|
+
const onDisk = (await readdir(sopsDir).catch(() => [])).filter((f) => /^guardrail-.+\.md$/.test(f));
|
|
700
|
+
const pruned = [];
|
|
701
|
+
const stale = [];
|
|
702
|
+
for (const file of onDisk) {
|
|
703
|
+
const stem = file.replace(/\.md$/, '');
|
|
704
|
+
if (servedStems.has(stem))
|
|
705
|
+
continue;
|
|
706
|
+
const filePath = join(sopsDir, file);
|
|
707
|
+
if (!opts.prune) {
|
|
708
|
+
say('warn', `${filePath}: no longer served (retired or rejected) — a proxy reading this directory still enforces it. Re-run with --prune to remove it.`);
|
|
709
|
+
stale.push(stem);
|
|
710
|
+
continue;
|
|
711
|
+
}
|
|
712
|
+
const existing = await readFile(filePath, 'utf-8').catch(() => null);
|
|
713
|
+
const parsed = existing !== null ? parseSopFile(existing) : null;
|
|
714
|
+
const clean = parsed !== null && parsed.contentHash !== null && parsed.contentHash === sha256(parsed.body);
|
|
715
|
+
if (!clean) {
|
|
716
|
+
say('warn', `${filePath}: no longer served but locally modified — left in place; remove it by hand.`);
|
|
717
|
+
stale.push(stem);
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
await unlink(filePath);
|
|
721
|
+
pruned.push(stem);
|
|
722
|
+
say('info', `${filePath}: removed (no longer served)`);
|
|
723
|
+
}
|
|
724
|
+
if (opts.json) {
|
|
725
|
+
emitJson({ sopsDir, written, unchanged, skipped, pruned, stale });
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
if (guardrails.length === 0)
|
|
729
|
+
log.info('No SHADOW or ENFORCING front-matter guardrails are served for this workspace.');
|
|
730
|
+
log.info(`${written.length} written, ${unchanged.length} unchanged, ${skipped.length} left untouched, ${pruned.length} pruned, ${stale.length} stale — in ${sopsDir}.`);
|
|
731
|
+
if (skipped.length > 0)
|
|
732
|
+
log.info('Re-run with --force to overwrite locally-modified guardrail files.');
|
|
733
|
+
log.dim('A proxy that reads .intutic/sops from disk (standalone, or INTUTIC_SOPS_DIR) enforces these within 30 s. A gateway-mode proxy reads the served projection directly and ignores this directory.');
|
|
734
|
+
}
|
|
735
|
+
//# sourceMappingURL=guardrails.js.map
|