@intutic/cli 1.10.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
package/dist/commands/policy.js
CHANGED
|
@@ -4,19 +4,46 @@ import * as os from 'node:os';
|
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { log } from '../lib/logger.js';
|
|
7
|
+
import { NOT_AUTHENTICATED } from '../lib/authMessages.js';
|
|
8
|
+
import { WASM_HOST_IMPORTS, unsupportedWasmImports, explainWasmImport, } from '@intutic/shared-types';
|
|
7
9
|
import { loadCredentials, loadConfig } from '../config/store.js';
|
|
8
10
|
import { resolveControlPlaneUrl } from '../config/paths.js';
|
|
9
11
|
import { createApiClient } from '../lib/api.js';
|
|
10
|
-
|
|
12
|
+
import { resolveSnapshotRulesPath, SNAPSHOT_RULES_FILE } from '../lib/policySnapshot.js';
|
|
13
|
+
/**
|
|
14
|
+
* Message of a caught value.
|
|
15
|
+
*
|
|
16
|
+
* `catch` binds `unknown` — a thrown non-Error (or an Error subclass with no
|
|
17
|
+
* message) is not hypothetical here, since `abort` below and the WASM runtime
|
|
18
|
+
* both throw across the host boundary.
|
|
19
|
+
*/
|
|
20
|
+
function errMessage(err) {
|
|
21
|
+
return err instanceof Error ? err.message : String(err);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The control plane these commands talk to, and the credentials for it.
|
|
25
|
+
*
|
|
26
|
+
* Lifted out of `getClient` because `policy snapshot` needs the workspace id and
|
|
27
|
+
* the raw key rather than an API client — and resolving them a second way is how
|
|
28
|
+
* one command ends up pointed at production while its neighbour is on --dev.
|
|
29
|
+
*/
|
|
30
|
+
async function resolveTarget(dev) {
|
|
11
31
|
const creds = await loadCredentials();
|
|
12
32
|
if (!creds) {
|
|
13
|
-
log.error(
|
|
33
|
+
log.error(NOT_AUTHENTICATED);
|
|
14
34
|
process.exit(1);
|
|
15
35
|
}
|
|
16
36
|
const config = loadConfig();
|
|
17
37
|
const devMode = dev || config?.devMode || process.env.INTUTIC_DEV === '1';
|
|
18
|
-
|
|
19
|
-
|
|
38
|
+
return {
|
|
39
|
+
controlPlaneUrl: resolveControlPlaneUrl(devMode),
|
|
40
|
+
apiKey: creds.apiKey,
|
|
41
|
+
workspaceId: creds.workspaceId,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function getClient(dev) {
|
|
45
|
+
const { controlPlaneUrl, apiKey } = await resolveTarget(dev);
|
|
46
|
+
return createApiClient(controlPlaneUrl, apiKey);
|
|
20
47
|
}
|
|
21
48
|
export async function runPolicyEnable(policyId, opts) {
|
|
22
49
|
log.header(`Intutic — Enable Policy: ${policyId}`);
|
|
@@ -35,7 +62,7 @@ export async function runPolicyEnable(policyId, opts) {
|
|
|
35
62
|
}
|
|
36
63
|
}
|
|
37
64
|
catch (err) {
|
|
38
|
-
log.error(`Failed to enable policy: ${err
|
|
65
|
+
log.error(`Failed to enable policy: ${errMessage(err)}`);
|
|
39
66
|
process.exit(1);
|
|
40
67
|
}
|
|
41
68
|
}
|
|
@@ -56,7 +83,7 @@ export async function runPolicyDisable(policyId, opts) {
|
|
|
56
83
|
}
|
|
57
84
|
}
|
|
58
85
|
catch (err) {
|
|
59
|
-
log.error(`Failed to disable policy: ${err
|
|
86
|
+
log.error(`Failed to disable policy: ${errMessage(err)}`);
|
|
60
87
|
process.exit(1);
|
|
61
88
|
}
|
|
62
89
|
}
|
|
@@ -82,7 +109,7 @@ export async function runPolicyRollback(policyId, opts) {
|
|
|
82
109
|
}
|
|
83
110
|
}
|
|
84
111
|
catch (err) {
|
|
85
|
-
log.error(`Failed to rollback policy: ${err
|
|
112
|
+
log.error(`Failed to rollback policy: ${errMessage(err)}`);
|
|
86
113
|
process.exit(1);
|
|
87
114
|
}
|
|
88
115
|
}
|
|
@@ -102,10 +129,55 @@ export async function runPolicyExport(opts) {
|
|
|
102
129
|
}
|
|
103
130
|
}
|
|
104
131
|
catch (err) {
|
|
105
|
-
log.error(`Failed to export policies: ${err
|
|
132
|
+
log.error(`Failed to export policies: ${errMessage(err)}`);
|
|
106
133
|
process.exit(1);
|
|
107
134
|
}
|
|
108
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* `intutic policy snapshot` — compile this workspace's policy to disk.
|
|
138
|
+
*
|
|
139
|
+
* The snapshot is the dynamic tier: every harness gate reads
|
|
140
|
+
* `~/.intutic/hooks/policy-snapshot.rules` and enforces the compiled floor and
|
|
141
|
+
* nothing else when it is missing. Until this command existed the only thing
|
|
142
|
+
* that ever wrote it was `intutic connect`, so arming the gates meant running a
|
|
143
|
+
* daemon that also rewrites five harnesses' hook configs, starts a drift
|
|
144
|
+
* watcher and provisions Valkey. This does the one thing.
|
|
145
|
+
*/
|
|
146
|
+
export async function runPolicySnapshot(opts) {
|
|
147
|
+
log.header('Intutic — Refresh Policy Snapshot');
|
|
148
|
+
const { controlPlaneUrl, apiKey, workspaceId } = await resolveTarget(opts.dev);
|
|
149
|
+
// The gates consult INTUTIC_SNAPSHOT_RULES before the default path, so writing
|
|
150
|
+
// to the default while they read elsewhere would produce a snapshot nothing
|
|
151
|
+
// enforces — the precise silent failure this command exists to end.
|
|
152
|
+
const rulesPath = resolveSnapshotRulesPath();
|
|
153
|
+
const snapshotDir = path.dirname(rulesPath);
|
|
154
|
+
if (path.basename(rulesPath) !== SNAPSHOT_RULES_FILE) {
|
|
155
|
+
log.warn(`INTUTIC_SNAPSHOT_RULES names "${path.basename(rulesPath)}", but the writer always produces ` +
|
|
156
|
+
`"${SNAPSHOT_RULES_FILE}" — the gates will read a file this command does not write.`);
|
|
157
|
+
}
|
|
158
|
+
// Imported here rather than at module scope: @intutic/sync-daemon pulls in ws,
|
|
159
|
+
// ioredis and chokidar, and every other `intutic policy` subcommand would pay
|
|
160
|
+
// that startup cost without ever touching a snapshot.
|
|
161
|
+
const { refreshPolicySnapshot } = await import('@intutic/sync-daemon');
|
|
162
|
+
const result = await refreshPolicySnapshot({ controlPlaneUrl, apiKey, workspaceId, snapshotDir });
|
|
163
|
+
// `refreshPolicySnapshot` never throws — policy refresh must not be able to
|
|
164
|
+
// take down the daemon's sync loop — so null is the only failure signal it
|
|
165
|
+
// has. Reporting that as success is how a machine comes to enforce nothing
|
|
166
|
+
// while the command meant to arm it said it worked.
|
|
167
|
+
if (!result) {
|
|
168
|
+
log.error('Snapshot NOT written — GET /api/v1/policy/resolve returned nothing usable.');
|
|
169
|
+
log.field('Control plane', controlPlaneUrl);
|
|
170
|
+
log.field('Workspace', workspaceId);
|
|
171
|
+
log.info('Any previous snapshot is untouched and still enforced.');
|
|
172
|
+
log.info('Check the control plane is reachable and the key is still valid: `intutic whoami`.');
|
|
173
|
+
process.exit(1);
|
|
174
|
+
}
|
|
175
|
+
log.success(`Policy snapshot written — ${result.ruleCount} rule(s) now enforced by every gate.`);
|
|
176
|
+
log.field('Digest', result.digest);
|
|
177
|
+
log.field('Workspace', workspaceId);
|
|
178
|
+
log.field('Path', path.join(snapshotDir, SNAPSHOT_RULES_FILE));
|
|
179
|
+
log.info('Confirm the gates accept it with `intutic doctor`.');
|
|
180
|
+
}
|
|
109
181
|
/**
|
|
110
182
|
* Minimal RequestContext used to validate a rule instantiates and evaluates
|
|
111
183
|
* before install. Mirrors packages/wasm-sdk/assembly/mock_context_allow.json
|
|
@@ -123,125 +195,276 @@ export const DEFAULT_ALLOW_MOCK = JSON.stringify({
|
|
|
123
195
|
risk_tier: 'Low',
|
|
124
196
|
dlp_findings: []
|
|
125
197
|
});
|
|
198
|
+
/**
|
|
199
|
+
* Every host function the proxy registers, declared once on this side.
|
|
200
|
+
*
|
|
201
|
+
* `tools/scripts/check-wasm-host-imports.js` asserts this equals
|
|
202
|
+
* `HOST_IMPORTS` in `packages/proxy/src/wasm/host.rs`. The two had already
|
|
203
|
+
* drifted — this side offered `seed`, the proxy never has — and the drift was
|
|
204
|
+
* invisible because it only surfaced as a per-request warning in a process
|
|
205
|
+
* nobody was reading.
|
|
206
|
+
*/
|
|
207
|
+
export const HOST_IMPORT_NAMES = WASM_HOST_IMPORTS;
|
|
208
|
+
/**
|
|
209
|
+
* A callable export, or null when the module does not export that name (or
|
|
210
|
+
* exports it as a global/table/memory instead of a function).
|
|
211
|
+
*
|
|
212
|
+
* The variadic signature is the widest thing every guest function here fits;
|
|
213
|
+
* `resolveGuestAbi` immediately narrows each one to its real arity by way of
|
|
214
|
+
* the `WasmGuestAbi` members it assigns them to.
|
|
215
|
+
*/
|
|
216
|
+
function exportedFunction(exports, name) {
|
|
217
|
+
const value = exports[name];
|
|
218
|
+
return typeof value === 'function' ? value : null;
|
|
219
|
+
}
|
|
220
|
+
/** Resolve the guest ABI, throwing a message that names the missing export. */
|
|
221
|
+
function resolveGuestAbi(instance) {
|
|
222
|
+
const { exports } = instance;
|
|
223
|
+
const memory = exports.memory;
|
|
224
|
+
if (!(memory instanceof WebAssembly.Memory)) {
|
|
225
|
+
// Previously read as `exports.memory as WebAssembly.Memory` and dereferenced,
|
|
226
|
+
// so a module without a memory export failed with "Cannot read properties of
|
|
227
|
+
// undefined (reading 'buffer')" instead of saying what was wrong.
|
|
228
|
+
throw new Error("WASM module does not export 'memory'. Compile with AssemblyScript's " +
|
|
229
|
+
'default settings (`--exportRuntime`) so the host can read what the rule returns.');
|
|
230
|
+
}
|
|
231
|
+
// Accept exactly the allocator exports the proxy runner accepts
|
|
232
|
+
// (packages/proxy/src/wasm/runner.rs: `allocate`, then `__new`) — anything
|
|
233
|
+
// looser would validate rules the proxy then silently fails open on.
|
|
234
|
+
let allocate;
|
|
235
|
+
const allocateExport = exportedFunction(exports, 'allocate');
|
|
236
|
+
if (allocateExport) {
|
|
237
|
+
allocate = allocateExport;
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
const newExport = exportedFunction(exports, '__new');
|
|
241
|
+
if (!newExport) {
|
|
242
|
+
throw new Error("WASM module is missing 'allocate' or '__new' memory helpers.");
|
|
243
|
+
}
|
|
244
|
+
allocate = (size) => newExport(size, 0);
|
|
245
|
+
}
|
|
246
|
+
const evaluate = exportedFunction(exports, 'evaluate');
|
|
247
|
+
if (!evaluate) {
|
|
248
|
+
throw new Error("WASM module is missing 'evaluate' function export.");
|
|
249
|
+
}
|
|
250
|
+
return { memory, allocate, evaluate };
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Read `len` UTF-8 bytes at `ptr` out of guest memory, or null if that range is
|
|
254
|
+
* not wholly inside it.
|
|
255
|
+
*/
|
|
256
|
+
function readGuestUtf8(memory, ptr, len) {
|
|
257
|
+
const size = memory.buffer.byteLength;
|
|
258
|
+
if (!Number.isInteger(ptr) || !Number.isInteger(len) || ptr < 0 || len < 0 || ptr + len > size) {
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
return Buffer.from(memory.buffer, ptr, len).toString('utf-8');
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Read an AssemblyScript string — UTF-16LE, with a 4-byte little-endian byte
|
|
265
|
+
* length in the header immediately before the pointer — out of guest memory.
|
|
266
|
+
* Returns null if the pointer or the length it declares is out of bounds.
|
|
267
|
+
*
|
|
268
|
+
* That bounds check is the point of the helper. The two copies of this loop it
|
|
269
|
+
* replaces trusted the header, which is a value the guest writes: a rule
|
|
270
|
+
* aborting with a bogus pointer could name a 4 GB string and send the CLI into
|
|
271
|
+
* a multi-billion-iteration loop building it one char at a time. The proxy host
|
|
272
|
+
* checks the same range and logs the raw pointer when it does not fit
|
|
273
|
+
* (`packages/proxy/src/wasm/host.rs`).
|
|
274
|
+
*/
|
|
275
|
+
function readGuestString(memory, ptr) {
|
|
276
|
+
const size = memory.buffer.byteLength;
|
|
277
|
+
if (!Number.isInteger(ptr) || ptr < 4 || ptr > size)
|
|
278
|
+
return null;
|
|
279
|
+
const byteLength = new DataView(memory.buffer).getUint32(ptr - 4, true);
|
|
280
|
+
if (byteLength % 2 !== 0 || ptr + byteLength > size)
|
|
281
|
+
return null;
|
|
282
|
+
return Buffer.from(memory.buffer, ptr, byteLength).toString('utf16le');
|
|
283
|
+
}
|
|
126
284
|
/**
|
|
127
285
|
* Instantiate a compiled WASM rule with the standard host imports and run
|
|
128
286
|
* `evaluate` against a JSON-serialized RequestContext. Returns the raw
|
|
129
|
-
* verdict code (0=ALLOW, 1=BLOCK, 2
|
|
287
|
+
* verdict code (0=ALLOW, 1=BLOCK, 3=REASK; 2 is a deprecated block). Throws on any instantiation or
|
|
130
288
|
* ABI failure — callers decide how to surface it.
|
|
131
289
|
*/
|
|
132
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Re-exported so existing callers and tests keep one import site.
|
|
292
|
+
*
|
|
293
|
+
* The implementations live in `@intutic/shared-types` because three surfaces
|
|
294
|
+
* need them — this CLI, the control plane's upload endpoint, and the SDK test
|
|
295
|
+
* harness — and each having its own copy is precisely how `seed` came to be
|
|
296
|
+
* offered here and never registered by the proxy.
|
|
297
|
+
*/
|
|
298
|
+
export const unsupportedImports = unsupportedWasmImports;
|
|
299
|
+
export const explainUnsupportedImport = explainWasmImport;
|
|
300
|
+
/**
|
|
301
|
+
* Instantiates a compiled rule with the proxy's host imports and evaluates it
|
|
302
|
+
* against one mock context, returning the raw verdict code.
|
|
303
|
+
*
|
|
304
|
+
* The import set here is the proxy's, not a convenient superset: a rule that
|
|
305
|
+
* links in this sandbox must link in the proxy, or install validation passes
|
|
306
|
+
* something the proxy then silently bypasses.
|
|
307
|
+
*/
|
|
308
|
+
export async function instantiateAndEvaluate(wasmBuffer, mockStr) {
|
|
133
309
|
let instanceRef = null;
|
|
310
|
+
/**
|
|
311
|
+
* Guest memory, or null before `WebAssembly.instantiate` has resolved.
|
|
312
|
+
*
|
|
313
|
+
* The null is load-bearing, not defensive padding: AssemblyScript's start
|
|
314
|
+
* function runs during instantiation and can call `abort` or `trace` from
|
|
315
|
+
* there, so a host import can be invoked before there is an instance to read
|
|
316
|
+
* memory from.
|
|
317
|
+
*/
|
|
318
|
+
const guestMemory = () => {
|
|
319
|
+
const memory = instanceRef?.exports.memory;
|
|
320
|
+
return memory instanceof WebAssembly.Memory ? memory : null;
|
|
321
|
+
};
|
|
134
322
|
const imports = {
|
|
135
323
|
env: {
|
|
136
324
|
// Mirrors the proxy's host import set (packages/proxy/src/wasm/host.rs)
|
|
137
325
|
// — a rule that links in the proxy must link here too, or install
|
|
138
326
|
// validation would reject rules the sandbox runs fine (and vice versa).
|
|
139
327
|
log_info(message, len) {
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
328
|
+
const memory = guestMemory();
|
|
329
|
+
if (!memory || !message)
|
|
330
|
+
return;
|
|
331
|
+
const text = readGuestUtf8(memory, message, len);
|
|
332
|
+
console.log(text === null
|
|
333
|
+
? `[WASM Log] <out of bounds: ptr=${message} len=${len}>`
|
|
334
|
+
: `[WASM Log] ${text}`);
|
|
145
335
|
},
|
|
336
|
+
// `fileName` is part of AssemblyScript's fixed abort ABI and deliberately
|
|
337
|
+
// unused: it points at the source path, which is only populated under
|
|
338
|
+
// --debug and is a bare pointer otherwise. Not renamed to `_fileName`
|
|
339
|
+
// because it is a real parameter of a foreign ABI and the name documents
|
|
340
|
+
// the slot; `args: 'after-used'` means an unused parameter before two used
|
|
341
|
+
// ones is not reported anyway.
|
|
146
342
|
abort(message, fileName, line, column) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const size = new Uint32Array(memory.buffer, message - 4, 1)[0];
|
|
151
|
-
const memView16 = new Uint16Array(memory.buffer);
|
|
152
|
-
const chars = [];
|
|
153
|
-
for (let i = 0; i < size / 2; i++) {
|
|
154
|
-
chars.push(String.fromCharCode(memView16[(message / 2) + i]));
|
|
155
|
-
}
|
|
156
|
-
errorMsg = chars.join('');
|
|
157
|
-
}
|
|
158
|
-
throw new Error(`WASM Abort: ${errorMsg} (at line ${line}, col ${column})`);
|
|
343
|
+
const memory = guestMemory();
|
|
344
|
+
const text = memory && message ? readGuestString(memory, message) : null;
|
|
345
|
+
throw new Error(`WASM Abort: ${text ?? 'AssemblyScript abort'} (at line ${line}, col ${column})`);
|
|
159
346
|
},
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
347
|
+
/**
|
|
348
|
+
* AssemblyScript's `trace(message, n, a0..a4)`: `n` says how many of the
|
|
349
|
+
* five f64 slots carry a value.
|
|
350
|
+
*
|
|
351
|
+
* The five slots were absent from this signature and `n` was accepted and
|
|
352
|
+
* dropped, so a rule calling `trace("tokens left", 1, remaining)` printed
|
|
353
|
+
* "tokens left" here and nothing else — the number the author was tracing
|
|
354
|
+
* never reached the output of the command whose whole job is to show them
|
|
355
|
+
* what their rule does. The proxy at least logs `n`
|
|
356
|
+
* (`packages/proxy/src/wasm/host.rs`).
|
|
357
|
+
*/
|
|
358
|
+
trace(message, n, a0, a1, a2, a3, a4) {
|
|
359
|
+
const memory = guestMemory();
|
|
360
|
+
const text = memory && message ? readGuestString(memory, message) : null;
|
|
361
|
+
// A module may declare a narrower `env.trace`, in which case JS passes
|
|
362
|
+
// undefined for the slots it did not declare; drop anything non-finite.
|
|
363
|
+
const count = Number.isInteger(n) ? Math.min(Math.max(n, 0), 5) : 0;
|
|
364
|
+
const values = [a0, a1, a2, a3, a4].slice(0, count).filter((v) => Number.isFinite(v));
|
|
365
|
+
const suffix = values.length > 0 ? ` ${values.join(' ')}` : '';
|
|
366
|
+
if (text === null) {
|
|
367
|
+
console.log(`[WASM Trace Pointer] ${message} (n=${n})${suffix}`);
|
|
170
368
|
}
|
|
171
369
|
else {
|
|
172
|
-
console.log(`[WASM Trace
|
|
370
|
+
console.log(`[WASM Trace] ${text}${suffix}`);
|
|
173
371
|
}
|
|
174
372
|
},
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
373
|
+
/**
|
|
374
|
+
* `env.read_referenced_file(pathPtr, pathLen, outPtr, outCap) -> i32`
|
|
375
|
+
*
|
|
376
|
+
* Always refuses here, and that is the honest answer rather than a gap:
|
|
377
|
+
* the proxy builds its readable-file table from the tool calls of a live
|
|
378
|
+
* request, and `intutic policy test` has a mock context, not a request.
|
|
379
|
+
* Pretending otherwise — serving the mock's paths off the developer's own
|
|
380
|
+
* disk — would make this harness disagree with production in the
|
|
381
|
+
* permissive direction, which is the failure the `runOnnxInference` mock
|
|
382
|
+
* below was removed for.
|
|
383
|
+
*
|
|
384
|
+
* -2 is ERR_REFUSED from `packages/proxy/src/wasm/referenced_files.rs`.
|
|
385
|
+
* A rule exercised here therefore takes its no-manifest-available branch,
|
|
386
|
+
* which is the branch worth checking anyway: it is what runs in any
|
|
387
|
+
* deployment that has not configured a manifest root.
|
|
388
|
+
*/
|
|
389
|
+
read_referenced_file(_pathPtr, _pathLen, _outPtr, _outCap) {
|
|
390
|
+
return -2;
|
|
391
|
+
},
|
|
392
|
+
// `seed()` used to be here, and the comment above — asserting this set
|
|
393
|
+
// mirrors the proxy's — was false two lines later. `host.rs` has never
|
|
394
|
+
// registered it. AssemblyScript emits `env.seed` for anything reaching
|
|
395
|
+
// `Math.random()`, so a rule using randomness validated here, installed,
|
|
396
|
+
// and then failed to link in the proxy on every request, where fail-open
|
|
397
|
+
// turned it into a silent allow. Removing it cannot make enforcement
|
|
398
|
+
// worse: such a rule was already fully bypassed, and now it is refused
|
|
399
|
+
// loudly at install instead of quietly at runtime.
|
|
400
|
+
//
|
|
401
|
+
// Not added to the proxy instead, deliberately: a governance verdict that
|
|
402
|
+
// samples is not a verdict. The same request would get different answers.
|
|
178
403
|
},
|
|
179
|
-
onnx_rules
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
return inputDataPtr;
|
|
192
|
-
}
|
|
193
|
-
}
|
|
404
|
+
// The `onnx_rules.runOnnxInference` mock lived here and is gone with the host
|
|
405
|
+
// import it imitated.
|
|
406
|
+
//
|
|
407
|
+
// It was worse than the production stub it stood for. The proxy's version returned
|
|
408
|
+
// the input pointer unchanged, so the rule it backed could never fire; this one,
|
|
409
|
+
// under --force-anomaly, rewrote the input buffer to 99.0 so the reconstruction
|
|
410
|
+
// error was enormous and the rule DID fire. A rule author could watch their
|
|
411
|
+
// sequence-anomaly rule block a request in `policy test`, ship it, and have it
|
|
412
|
+
// never block anything in production. A harness that disagrees with production in
|
|
413
|
+
// the permissive direction is worse than no harness.
|
|
194
414
|
};
|
|
195
|
-
|
|
415
|
+
// `Uint8Array` means `Uint8Array<ArrayBufferLike>`, and `BufferSource` demands
|
|
416
|
+
// `ArrayBufferView<ArrayBuffer>` — a view over a SharedArrayBuffer is not one,
|
|
417
|
+
// so the parameter type callers actually use (`Buffer` from `fs.readFile`) does
|
|
418
|
+
// not fit `WebAssembly.instantiate`. The old `as any` on the result hid that
|
|
419
|
+
// mismatch by silently selecting the `Module` overload instead, which returns a
|
|
420
|
+
// bare `Instance` with no `.instance` property: the destructuring below was
|
|
421
|
+
// reading a field the declared type does not have. Copy into a plainly-backed
|
|
422
|
+
// array rather than narrowing the parameter, which would reject `Buffer`. One
|
|
423
|
+
// copy of a rule binary, once per validation.
|
|
424
|
+
// Compile first and inspect the import section, so an unprovided import is
|
|
425
|
+
// named rather than surfacing as V8's "Import #3 module=env function=seed
|
|
426
|
+
// error: function import requires a callable". Same outcome either way — the
|
|
427
|
+
// instantiate below would throw — but a rule author needs to know it was
|
|
428
|
+
// `Math.random()`.
|
|
429
|
+
const compiled = await WebAssembly.compile(new Uint8Array(wasmBuffer));
|
|
430
|
+
const unsupported = unsupportedImports(compiled);
|
|
431
|
+
if (unsupported.length > 0) {
|
|
432
|
+
throw new Error(`rule imports ${unsupported.length} function(s) the proxy sandbox does not provide:\n` +
|
|
433
|
+
unsupported.map((n) => ` ${explainUnsupportedImport(n)}`).join('\n') +
|
|
434
|
+
`\n Available: ${HOST_IMPORT_NAMES.map((n) => `env.${n}`).join(', ')}.`);
|
|
435
|
+
}
|
|
436
|
+
const instance = await WebAssembly.instantiate(compiled, imports);
|
|
196
437
|
instanceRef = instance;
|
|
438
|
+
const abi = resolveGuestAbi(instance);
|
|
197
439
|
const jsonBytes = Buffer.from(mockStr);
|
|
198
|
-
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
|
|
202
|
-
if (typeof instance.exports.allocate === 'function') {
|
|
203
|
-
offset = instance.exports.allocate(jsonBytes.length);
|
|
204
|
-
}
|
|
205
|
-
else if (typeof instance.exports.__new === 'function') {
|
|
206
|
-
offset = instance.exports.__new(jsonBytes.length, 0);
|
|
207
|
-
}
|
|
208
|
-
else {
|
|
209
|
-
throw new Error("WASM module is missing 'allocate' or '__new' memory helpers.");
|
|
210
|
-
}
|
|
211
|
-
const memory = instance.exports.memory;
|
|
212
|
-
const memView = new Uint8Array(memory.buffer, offset, jsonBytes.length);
|
|
440
|
+
const offset = abi.allocate(jsonBytes.length);
|
|
441
|
+
// Re-read `abi.memory.buffer` here rather than caching a view: `allocate` can
|
|
442
|
+
// grow the memory, which detaches every ArrayBuffer taken before the call.
|
|
443
|
+
const memView = new Uint8Array(abi.memory.buffer, offset, jsonBytes.length);
|
|
213
444
|
memView.set(jsonBytes);
|
|
214
|
-
|
|
215
|
-
if (typeof evaluate !== 'function') {
|
|
216
|
-
throw new Error("WASM module is missing 'evaluate' function export.");
|
|
217
|
-
}
|
|
218
|
-
return evaluate(offset, jsonBytes.length);
|
|
445
|
+
return abi.evaluate(offset, jsonBytes.length);
|
|
219
446
|
}
|
|
220
447
|
export async function runPolicyTest(opts) {
|
|
221
448
|
log.header('Intutic — Test Local WASM Policy Rule');
|
|
222
449
|
let wasmBuffer;
|
|
223
450
|
let mockStr;
|
|
224
|
-
let forceAnomaly = false;
|
|
225
451
|
try {
|
|
226
452
|
wasmBuffer = await fs.readFile(opts.wasm);
|
|
227
453
|
}
|
|
228
454
|
catch (err) {
|
|
229
|
-
log.error(`Failed to read WASM file at "${opts.wasm}": ${err
|
|
455
|
+
log.error(`Failed to read WASM file at "${opts.wasm}": ${errMessage(err)}`);
|
|
230
456
|
process.exit(1);
|
|
231
457
|
}
|
|
232
458
|
try {
|
|
233
459
|
mockStr = await fs.readFile(opts.mock, 'utf-8');
|
|
234
|
-
|
|
235
|
-
if (parsed && parsed.mock_anomaly === true) {
|
|
236
|
-
forceAnomaly = true;
|
|
237
|
-
}
|
|
460
|
+
JSON.parse(mockStr); // syntax check
|
|
238
461
|
}
|
|
239
462
|
catch (err) {
|
|
240
|
-
log.error(`Failed to read or parse mock context JSON at "${opts.mock}": ${err
|
|
463
|
+
log.error(`Failed to read or parse mock context JSON at "${opts.mock}": ${errMessage(err)}`);
|
|
241
464
|
process.exit(1);
|
|
242
465
|
}
|
|
243
466
|
try {
|
|
244
|
-
const verdict = await instantiateAndEvaluate(wasmBuffer, mockStr
|
|
467
|
+
const verdict = await instantiateAndEvaluate(wasmBuffer, mockStr);
|
|
245
468
|
log.info(`Dry-run evaluation executed successfully.`);
|
|
246
469
|
log.field('WASM Verdict Code', String(verdict));
|
|
247
470
|
if (verdict === 0) {
|
|
@@ -250,15 +473,23 @@ export async function runPolicyTest(opts) {
|
|
|
250
473
|
else if (verdict === 1) {
|
|
251
474
|
log.warn('Result: BLOCK / KILL');
|
|
252
475
|
}
|
|
476
|
+
else if (verdict === 3) {
|
|
477
|
+
log.warn('Result: REASK — the agent is told why and may retry');
|
|
478
|
+
}
|
|
253
479
|
else if (verdict === 2) {
|
|
254
|
-
log.warn('Result:
|
|
480
|
+
log.warn('Result: BLOCK (deprecated code 2)');
|
|
481
|
+
log.info(' 2 was specified as REDACT and never could be: a rule is not given');
|
|
482
|
+
log.info(' the request body, so there is nothing to redact. The proxy treats it');
|
|
483
|
+
log.info(' as a block. Return 1 to block, or 3 to reask.');
|
|
255
484
|
}
|
|
256
485
|
else {
|
|
257
|
-
log.error(`Result:
|
|
486
|
+
log.error(`Result: unmapped verdict code ${verdict}`);
|
|
487
|
+
log.info(' Valid codes are 0 (allow), 1 (block) and 3 (reask). The proxy allows');
|
|
488
|
+
log.info(' anything else, so a rule returning this enforces nothing.');
|
|
258
489
|
}
|
|
259
490
|
}
|
|
260
491
|
catch (err) {
|
|
261
|
-
log.error(`Execution error during WASM policy test: ${err
|
|
492
|
+
log.error(`Execution error during WASM policy test: ${errMessage(err)}`);
|
|
262
493
|
process.exit(1);
|
|
263
494
|
}
|
|
264
495
|
}
|
|
@@ -312,10 +543,123 @@ export function buildCompileArgs(opts) {
|
|
|
312
543
|
}
|
|
313
544
|
return args;
|
|
314
545
|
}
|
|
546
|
+
/** Where a fetched candidate source is written: two directories below the project root, which is what its `../../assembly/index` import needs. */
|
|
547
|
+
export function candidateSourcePath(candidateId) {
|
|
548
|
+
return path.join('generated', 'candidates', `${candidateId}.ts`);
|
|
549
|
+
}
|
|
550
|
+
export function candidateOutPath(candidateId) {
|
|
551
|
+
return path.join('build', `${candidateId}.wasm`);
|
|
552
|
+
}
|
|
553
|
+
export function sourceSha256Of(source) {
|
|
554
|
+
return createHash('sha256').update(source, 'utf8').digest('hex');
|
|
555
|
+
}
|
|
556
|
+
/**
|
|
557
|
+
* Fetch a candidate's source of record and check it against the hash the
|
|
558
|
+
* server states. A body that does not hash to its own `sourceSha256` is not
|
|
559
|
+
* compiled: `/bundle` would refuse the result anyway, and a mismatch here
|
|
560
|
+
* means something between the control plane and this process changed bytes.
|
|
561
|
+
*/
|
|
562
|
+
export async function fetchCandidateSource(client, candidateId) {
|
|
563
|
+
const { status, body } = await client.getWithStatus(`/api/v1/rule-candidates/${encodeURIComponent(candidateId)}/source`);
|
|
564
|
+
if (status === 404) {
|
|
565
|
+
log.error(`Rule candidate "${candidateId}" was not found in this workspace.`);
|
|
566
|
+
process.exit(1);
|
|
567
|
+
}
|
|
568
|
+
if (status === 409) {
|
|
569
|
+
log.error(`Refused: ${body.error ?? 'conflict'}${body.detail ? ` — ${body.detail}` : ''}`);
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
if (status !== 200 || typeof body.source !== 'string' || typeof body.sourceSha256 !== 'string') {
|
|
573
|
+
log.error(`Could not fetch the candidate's source (HTTP ${status}): ${body.error ?? 'unexpected response'}`);
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
const local = sourceSha256Of(body.source);
|
|
577
|
+
if (local !== body.sourceSha256.toLowerCase()) {
|
|
578
|
+
log.error(`The served source hashes to ${local}, not the ${body.sourceSha256} the server states; refusing to compile it.`);
|
|
579
|
+
process.exit(1);
|
|
580
|
+
}
|
|
581
|
+
return {
|
|
582
|
+
candidateId: body.candidateId ?? candidateId,
|
|
583
|
+
guardrailId: body.guardrailId ?? null,
|
|
584
|
+
status: body.status ?? 'unknown',
|
|
585
|
+
source: body.source,
|
|
586
|
+
sourceSha256: body.sourceSha256.toLowerCase(),
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* Upload a compiled bundle to its candidate together with the hash of the
|
|
591
|
+
* source it was compiled from, and print the gate results. The server
|
|
592
|
+
* recomputes the hash from the candidate row; a bundle built from anything
|
|
593
|
+
* else is refused before a gate runs.
|
|
594
|
+
*/
|
|
595
|
+
export async function uploadCandidateBundle(client, candidateId, wasmPath, sourceSha256) {
|
|
596
|
+
let bytes;
|
|
597
|
+
try {
|
|
598
|
+
bytes = await fs.readFile(wasmPath);
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
log.error(`Failed to read the compiled bundle at "${wasmPath}": ${errMessage(err)}`);
|
|
602
|
+
process.exit(1);
|
|
603
|
+
}
|
|
604
|
+
const form = new FormData();
|
|
605
|
+
form.append('file', new Blob([new Uint8Array(bytes)], { type: 'application/wasm' }), path.basename(wasmPath));
|
|
606
|
+
form.append('source_sha256', sourceSha256);
|
|
607
|
+
const { status, body } = await client.postForm(`/api/v1/rule-candidates/${encodeURIComponent(candidateId)}/bundle`, form);
|
|
608
|
+
if (status === 404) {
|
|
609
|
+
log.error(`Rule candidate "${candidateId}" was not found in this workspace.`);
|
|
610
|
+
process.exit(1);
|
|
611
|
+
}
|
|
612
|
+
if (status === 400 || status === 409) {
|
|
613
|
+
log.error(`Refused: ${body.error ?? `HTTP ${status}`}${body.detail ? ` — ${body.detail}` : ''}`);
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
if (status !== 200 && status !== 201) {
|
|
617
|
+
log.error(`Upload failed (HTTP ${status}): ${body.error ?? 'unexpected response'}`);
|
|
618
|
+
process.exit(1);
|
|
619
|
+
}
|
|
620
|
+
for (const g of body.gates ?? [])
|
|
621
|
+
log.info(` ${g.passed ? '✓' : '✗'} ${g.gate}: ${g.detail}`);
|
|
622
|
+
if (body.accepted) {
|
|
623
|
+
log.success(`Accepted: rule ${body.ruleId ?? '?'} is in ${body.mode ?? 'SHADOW'} for candidate ${candidateId}.`);
|
|
624
|
+
log.info('It reports without acting until a member promotes it on counted shadow evidence.');
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
log.error('The bundle failed verification; the candidate is rejected with the gate named above.');
|
|
628
|
+
process.exit(1);
|
|
629
|
+
}
|
|
315
630
|
export async function runPolicyCompile(opts) {
|
|
316
631
|
log.header('Intutic — Compile WASM Policy Rule');
|
|
317
|
-
|
|
318
|
-
|
|
632
|
+
let src = opts.src;
|
|
633
|
+
let out = opts.out ?? 'build/rule.wasm';
|
|
634
|
+
let candidate = null;
|
|
635
|
+
let client = null;
|
|
636
|
+
if (opts.candidate) {
|
|
637
|
+
if (opts.src) {
|
|
638
|
+
log.error('--candidate compiles the source of record; it cannot be combined with --src.');
|
|
639
|
+
process.exit(1);
|
|
640
|
+
}
|
|
641
|
+
try {
|
|
642
|
+
await fs.access(path.join('assembly', 'index.ts'));
|
|
643
|
+
}
|
|
644
|
+
catch {
|
|
645
|
+
log.error('Run this from a rule project with assembly/index.ts (the SDK layout): the generated source imports the SDK from two directories up.');
|
|
646
|
+
process.exit(1);
|
|
647
|
+
}
|
|
648
|
+
client = await getClient(opts.dev);
|
|
649
|
+
candidate = await fetchCandidateSource(client, opts.candidate);
|
|
650
|
+
src = candidateSourcePath(opts.candidate);
|
|
651
|
+
if (!opts.out)
|
|
652
|
+
out = candidateOutPath(opts.candidate);
|
|
653
|
+
await fs.mkdir(path.dirname(src), { recursive: true });
|
|
654
|
+
await fs.writeFile(src, candidate.source, 'utf8');
|
|
655
|
+
log.info(`Source of record for ${candidate.candidateId} (${candidate.status}) → ${src}`);
|
|
656
|
+
log.info(`source_sha256 ${candidate.sourceSha256}`);
|
|
657
|
+
}
|
|
658
|
+
else if (opts.upload) {
|
|
659
|
+
log.error('--upload needs --candidate: only a candidate has a source of record to upload against.');
|
|
660
|
+
process.exit(1);
|
|
661
|
+
}
|
|
662
|
+
const args = buildCompileArgs({ src, out, debug: opts.debug });
|
|
319
663
|
try {
|
|
320
664
|
await fs.mkdir(path.dirname(out), { recursive: true });
|
|
321
665
|
}
|
|
@@ -333,8 +677,16 @@ export async function runPolicyCompile(opts) {
|
|
|
333
677
|
log.info(' pnpm add -D assemblyscript assemblyscript-json');
|
|
334
678
|
process.exit(1);
|
|
335
679
|
}
|
|
336
|
-
log.success(`Compiled ${
|
|
337
|
-
|
|
680
|
+
log.success(`Compiled ${src ?? 'assembly/index.ts'} → ${out}`);
|
|
681
|
+
if (!candidate || !opts.candidate) {
|
|
682
|
+
log.info('Next: dry-run with `intutic policy test --wasm <out> --mock <mock.json>`');
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (!opts.upload) {
|
|
686
|
+
log.info(`Next: intutic policy compile --candidate ${opts.candidate} --upload (or upload ${out} with source_sha256 ${candidate.sourceSha256})`);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
await uploadCandidateBundle(client, opts.candidate, out, candidate.sourceSha256);
|
|
338
690
|
}
|
|
339
691
|
export async function runPolicyInstall(opts) {
|
|
340
692
|
log.header('Intutic — Install Local WASM Policy Rule');
|
|
@@ -343,16 +695,34 @@ export async function runPolicyInstall(opts) {
|
|
|
343
695
|
wasmBuffer = await fs.readFile(opts.wasm);
|
|
344
696
|
}
|
|
345
697
|
catch (err) {
|
|
346
|
-
log.error(`Failed to read WASM file at "${opts.wasm}": ${err
|
|
698
|
+
log.error(`Failed to read WASM file at "${opts.wasm}": ${errMessage(err)}`);
|
|
347
699
|
process.exit(1);
|
|
348
700
|
}
|
|
349
701
|
// Refuse to install a rule that cannot instantiate or evaluate — a broken
|
|
350
702
|
// rule enforces nothing (the proxy sandbox fails open).
|
|
351
703
|
try {
|
|
352
|
-
await instantiateAndEvaluate(wasmBuffer, DEFAULT_ALLOW_MOCK);
|
|
704
|
+
const validationVerdict = await instantiateAndEvaluate(wasmBuffer, DEFAULT_ALLOW_MOCK);
|
|
705
|
+
// Refuse a code the proxy does not map. Everything outside {0,1,2,3} is
|
|
706
|
+
// allowed at runtime with a warning, so a rule inventing a rung installs
|
|
707
|
+
// clean and then enforces nothing — the same silent shape as a rule that
|
|
708
|
+
// cannot link. 2 is accepted here because already-installed rules use it,
|
|
709
|
+
// but it is deprecated and reported as such.
|
|
710
|
+
if (validationVerdict === 2) {
|
|
711
|
+
// Accepted, because refusing it would break reinstalling a rule that
|
|
712
|
+
// already shipped — but never silently. The guest never receives the
|
|
713
|
+
// request body, so redaction was never expressible, and the proxy maps 2
|
|
714
|
+
// to a block. An author who believes they are redacting is blocking.
|
|
715
|
+
log.warn('Rule returned verdict code 2 (REDACT), which is deprecated. The proxy treats it ' +
|
|
716
|
+
'as a block. Return 1 to block, or 3 to reask.');
|
|
717
|
+
}
|
|
718
|
+
if (![0, 1, 2, 3].includes(validationVerdict)) {
|
|
719
|
+
log.error(`Rule returned verdict code ${validationVerdict}, which the proxy does not map — ` +
|
|
720
|
+
'it would be allowed on every request. Valid codes: 0 allow, 1 block, 3 reask.');
|
|
721
|
+
process.exit(1);
|
|
722
|
+
}
|
|
353
723
|
}
|
|
354
724
|
catch (err) {
|
|
355
|
-
log.error(`Rule failed validation and was NOT installed: ${err
|
|
725
|
+
log.error(`Rule failed validation and was NOT installed: ${errMessage(err)}`);
|
|
356
726
|
process.exit(1);
|
|
357
727
|
}
|
|
358
728
|
const priority = parseInt(opts.priority ?? '100', 10);
|
|
@@ -369,7 +739,7 @@ export async function runPolicyInstall(opts) {
|
|
|
369
739
|
await fs.writeFile(dest, wasmBuffer);
|
|
370
740
|
}
|
|
371
741
|
catch (err) {
|
|
372
|
-
log.error(`Failed to install rule to "${dest}": ${err
|
|
742
|
+
log.error(`Failed to install rule to "${dest}": ${errMessage(err)}`);
|
|
373
743
|
process.exit(1);
|
|
374
744
|
}
|
|
375
745
|
const sha256 = createHash('sha256').update(wasmBuffer).digest('hex');
|
|
@@ -380,6 +750,77 @@ export async function runPolicyInstall(opts) {
|
|
|
380
750
|
log.info('The proxy picks up local rule changes within ~5 s on the next request.');
|
|
381
751
|
log.info('If your proxy config.yaml sets intutic_settings.wasm_local_dir, make sure it matches this path (or set INTUTIC_WASM_DIR for both).');
|
|
382
752
|
}
|
|
753
|
+
const VERDICT_NAMES = {
|
|
754
|
+
'0': 'allow',
|
|
755
|
+
'1': 'block',
|
|
756
|
+
'2': 'block (deprecated code 2)',
|
|
757
|
+
'3': 'reask',
|
|
758
|
+
};
|
|
759
|
+
/**
|
|
760
|
+
* `intutic policy replay <ruleId>` — "prove before enforce": run an installed
|
|
761
|
+
* or not-yet-active rule against a sample of this workspace's own recent
|
|
762
|
+
* traffic and report what it would have done, without touching enforcement.
|
|
763
|
+
*
|
|
764
|
+
* Uses `postWithStatus` rather than `post`: the 422 the route returns for too
|
|
765
|
+
* little sampled traffic is an answer this command exists to report, not a
|
|
766
|
+
* request failure to throw past.
|
|
767
|
+
*/
|
|
768
|
+
export async function runPolicyReplay(ruleId, opts) {
|
|
769
|
+
log.header(`Intutic — Replay Policy Rule: ${ruleId}`);
|
|
770
|
+
const client = await getClient(opts.dev);
|
|
771
|
+
const body = {};
|
|
772
|
+
if (opts.limit !== undefined) {
|
|
773
|
+
const limit = parseInt(opts.limit, 10);
|
|
774
|
+
if (isNaN(limit) || limit <= 0) {
|
|
775
|
+
log.error(`Invalid --limit "${opts.limit}". Must be a positive integer.`);
|
|
776
|
+
process.exit(1);
|
|
777
|
+
}
|
|
778
|
+
body.limit = limit;
|
|
779
|
+
}
|
|
780
|
+
if (opts.since !== undefined) {
|
|
781
|
+
body.since = opts.since;
|
|
782
|
+
}
|
|
783
|
+
try {
|
|
784
|
+
const { status, body: result } = await client.postWithStatus(`/api/v1/wasm-rules/${ruleId}/replay`, body);
|
|
785
|
+
if (status === 404) {
|
|
786
|
+
log.error(`Rule "${ruleId}" not found in this workspace.`);
|
|
787
|
+
process.exit(1);
|
|
788
|
+
}
|
|
789
|
+
if (status === 422) {
|
|
790
|
+
const err = result;
|
|
791
|
+
if (err.error === 'not_enough_traffic') {
|
|
792
|
+
log.warn('Not enough sampled traffic to estimate a would-act rate.');
|
|
793
|
+
log.field('Sampled contexts', String(err.contextCount ?? 0));
|
|
794
|
+
log.field('Required', String(err.minRequired ?? 0));
|
|
795
|
+
log.info('Contexts are sampled at WASM_CONTEXT_SNAPSHOT_RATE (default 5%) as traffic flows ' +
|
|
796
|
+
'through the proxy — this grows with time and traffic, not with --limit.');
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
log.error(`Replay could not evaluate the rule: ${err.message ?? err.error}`);
|
|
800
|
+
}
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
if (status !== 200) {
|
|
804
|
+
log.error(`Replay failed (${status}): ${JSON.stringify(result)}`);
|
|
805
|
+
process.exit(1);
|
|
806
|
+
}
|
|
807
|
+
const summary = result;
|
|
808
|
+
log.success(`Replayed against ${summary.contextCount} sampled context(s).`);
|
|
809
|
+
log.field('Since', summary.since ?? '(all sampled history)');
|
|
810
|
+
log.field('Would act (block/reask/other)', `${summary.wouldActCount} (${(summary.wouldActRate * 100).toFixed(1)}%)`);
|
|
811
|
+
for (const [code, count] of Object.entries(summary.verdictCounts)) {
|
|
812
|
+
log.field(` verdict ${code} (${VERDICT_NAMES[code] ?? 'unmapped'})`, String(count));
|
|
813
|
+
}
|
|
814
|
+
if (summary.sampleMatches.length > 0) {
|
|
815
|
+
log.info(`First ${summary.sampleMatches.length} non-allow context(s):`);
|
|
816
|
+
console.log(JSON.stringify(summary.sampleMatches, null, 2));
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
catch (err) {
|
|
820
|
+
log.error(`Failed to replay rule: ${errMessage(err)}`);
|
|
821
|
+
process.exit(1);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
383
824
|
export async function runPolicyListLocal() {
|
|
384
825
|
log.header('Intutic — Local WASM Policy Rules');
|
|
385
826
|
const dir = resolveLocalWasmDir();
|
|
@@ -405,7 +846,7 @@ export async function runPolicyListLocal() {
|
|
|
405
846
|
log.field(name, `priority=${priority} size=${stat.size}B mtime=${stat.mtime.toISOString()} sha256=${sha256.slice(0, 16)}…`);
|
|
406
847
|
}
|
|
407
848
|
catch (err) {
|
|
408
|
-
log.warn(`${file}: unreadable (${err
|
|
849
|
+
log.warn(`${file}: unreadable (${errMessage(err)})`);
|
|
409
850
|
}
|
|
410
851
|
}
|
|
411
852
|
}
|