@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/connect.js
CHANGED
|
@@ -16,17 +16,27 @@ const { version: cliPkgVersion } = createRequire(import.meta.url)('../../package
|
|
|
16
16
|
import * as node_fs from 'node:fs/promises';
|
|
17
17
|
import { log } from '../lib/logger.js';
|
|
18
18
|
import { ensureValkey, valkeyRemediation, isValkeyRunning } from '../lib/ensureValkey.js';
|
|
19
|
+
import { createShutdownHandler, terminateChild } from '../lib/gracefulShutdown.js';
|
|
19
20
|
import { loadCredentials, loadConfig, saveConfig, loadIntegrity, saveIntegrity, } from '../config/store.js';
|
|
20
21
|
import { resolveControlPlaneUrl, getIntuticDir } from '../config/paths.js';
|
|
21
22
|
import { createApiClient } from '../lib/api.js';
|
|
22
23
|
import { getActiveAgentProcesses } from '../lib/process.js';
|
|
23
24
|
import { getAdapter } from '../harness/detector.js';
|
|
24
25
|
import { printOnboardingGuide } from '../lib/onboarding.js';
|
|
26
|
+
import { writeEnforcementState } from '../lib/enforcementState.js';
|
|
27
|
+
import { reportDeviceState } from '../lib/deviceReport.js';
|
|
28
|
+
import { parseChecksums, verifyChecksum } from '../lib/binaryChecksum.js';
|
|
25
29
|
import { newIso } from '@intutic/id';
|
|
26
30
|
import pc from 'picocolors';
|
|
27
|
-
import { SyncWsClient, startWatcher, updatePreToolUseHooks, injectMcpServer, guardSettingsFile, writeRuntimeEnv, runComplianceProbes, drainHookEvents, syncOfflineTraces, TrajectoryMonitor, } from '@intutic/sync-daemon';
|
|
31
|
+
import { SyncWsClient, startWatcher, updatePreToolUseHooks, injectMcpServer, guardSettingsFile, warnIfDshCoverageGap, writeRuntimeEnv, refreshPolicySnapshot, refreshGateCaches, localHoldTokensFor, runComplianceProbes, drainHookEvents, drainReviewRequests, REVIEW_REQUESTS_LOG, syncOfflineTraces, TrajectoryMonitor, collectAgentReport, reportAgent, fetchLocalProxyInstanceId, startHarnessSession, endAllOpenSessions, } from '@intutic/sync-daemon';
|
|
28
32
|
import { watch } from 'chokidar';
|
|
29
|
-
|
|
33
|
+
// Named, not default: under `module: Node16` TypeScript resolves ioredis's
|
|
34
|
+
// default export to the module namespace rather than the class, so `new
|
|
35
|
+
// Redis(...)` is not constructable and `Redis` is not a type. That is what the
|
|
36
|
+
// `new (Redis as any)(...)` below was working around, at the cost of also
|
|
37
|
+
// giving up the client's type. `packages/mcp-proxy` already imports it this
|
|
38
|
+
// way; both forms are the same class at run time.
|
|
39
|
+
import { Redis } from 'ioredis';
|
|
30
40
|
import * as net from 'node:net';
|
|
31
41
|
import { spawn, execSync } from 'node:child_process';
|
|
32
42
|
import { createWriteStream } from 'node:fs';
|
|
@@ -49,26 +59,54 @@ function isPortInUse(port) {
|
|
|
49
59
|
server.listen(port, '127.0.0.1');
|
|
50
60
|
});
|
|
51
61
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Is this a path the tamper guard should inspect?
|
|
64
|
+
*
|
|
65
|
+
* Deliberately broad: `guardSettingsFile` already branches per harness, so the only
|
|
66
|
+
* job here is to stop filtering out paths it knows how to handle.
|
|
67
|
+
*/
|
|
68
|
+
function isGovernedConfigPath(changedPath, filename) {
|
|
69
|
+
if (filename === 'settings.json' || filename === 'settings.local.json')
|
|
70
|
+
return true;
|
|
71
|
+
if (filename === 'hooks.json')
|
|
72
|
+
return true;
|
|
73
|
+
// Goose's immutable governance plugin.
|
|
74
|
+
if (changedPath.includes('intutic-governance'))
|
|
75
|
+
return true;
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Asset names MUST match publish.yml's build-rust-proxy matrix
|
|
80
|
+
* artifact_name — mirrors packages/proxy/bin/proxy.js's `resolveAssetName`
|
|
81
|
+
* exactly (same five names, same live-release verification: `gh release
|
|
82
|
+
* view` on v1.6.0 through the current release confirmed every one of
|
|
83
|
+
* these). `platform`/`arch` are parameters, not read from `process`
|
|
84
|
+
* directly, so connect.test.ts can exercise every combination without
|
|
85
|
+
* stubbing global process state.
|
|
86
|
+
*/
|
|
87
|
+
export function resolveProxyAssetName(platform = process.platform, arch = process.arch) {
|
|
56
88
|
if (platform === 'darwin') {
|
|
57
89
|
if (arch === 'arm64')
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
90
|
+
return 'intutic-proxy-darwin-arm64';
|
|
91
|
+
if (arch === 'x64')
|
|
92
|
+
return 'intutic-proxy-darwin-x64';
|
|
61
93
|
}
|
|
62
94
|
else if (platform === 'linux') {
|
|
63
95
|
if (arch === 'x64')
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
96
|
+
return 'intutic-proxy-linux-x64';
|
|
97
|
+
if (arch === 'arm64')
|
|
98
|
+
return 'intutic-proxy-linux-arm64';
|
|
67
99
|
}
|
|
68
100
|
else if (platform === 'win32') {
|
|
69
101
|
if (arch === 'x64')
|
|
70
|
-
|
|
102
|
+
return 'intutic-proxy-win32-x64.exe';
|
|
71
103
|
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
async function downloadProxyBinary(destPath) {
|
|
107
|
+
const platform = process.platform;
|
|
108
|
+
const arch = process.arch;
|
|
109
|
+
const assetName = resolveProxyAssetName(platform, arch);
|
|
72
110
|
if (!assetName) {
|
|
73
111
|
throw new Error(`Unsupported platform/architecture: ${platform}-${arch}`);
|
|
74
112
|
}
|
|
@@ -89,6 +127,16 @@ async function downloadProxyBinary(destPath) {
|
|
|
89
127
|
}
|
|
90
128
|
const arrayBuffer = await response.arrayBuffer();
|
|
91
129
|
const buffer = Buffer.from(arrayBuffer);
|
|
130
|
+
log.info('Verifying checksum...');
|
|
131
|
+
const checksumsUrl = `https://github.com/intutic/intutic/releases/download/v${cliPkgVersion}/checksums.json`;
|
|
132
|
+
const checksumsResponse = await fetch(checksumsUrl);
|
|
133
|
+
if (!checksumsResponse.ok) {
|
|
134
|
+
throw new Error(`Failed to download checksums.json (HTTP ${checksumsResponse.status} ${checksumsResponse.statusText}).\n` +
|
|
135
|
+
` URL: ${checksumsUrl}\n` +
|
|
136
|
+
`Refusing to install an unverified binary.`);
|
|
137
|
+
}
|
|
138
|
+
const checksums = parseChecksums(await checksumsResponse.text());
|
|
139
|
+
verifyChecksum(buffer, checksums, assetName);
|
|
92
140
|
const destDir = node_path.dirname(destPath);
|
|
93
141
|
await node_fs.mkdir(destDir, { recursive: true });
|
|
94
142
|
await node_fs.writeFile(destPath, buffer);
|
|
@@ -136,11 +184,23 @@ export async function runConnect(opts) {
|
|
|
136
184
|
}
|
|
137
185
|
let config = loadConfig();
|
|
138
186
|
if (!config && opts.workspaceId && opts.apiKey) {
|
|
187
|
+
// `IntuticConfig`, not `as any`. This literal used to carry a
|
|
188
|
+
// `workspaceId` too, which the type does not declare and nothing reads —
|
|
189
|
+
// `init.ts`, the only other writer of ~/.intutic/config.json, has never
|
|
190
|
+
// written one, and every consumer takes the workspace id from the
|
|
191
|
+
// credentials instead. The cast was there to admit it, so it went with the
|
|
192
|
+
// cast. The workspace id from --workspace-id still reaches everything that
|
|
193
|
+
// needs it, via `creds` above.
|
|
139
194
|
config = {
|
|
140
|
-
workspaceId: opts.workspaceId,
|
|
141
195
|
harnesses: [],
|
|
142
196
|
configVersion: 0,
|
|
143
197
|
devMode: opts.dev || false,
|
|
198
|
+
// workspaceRoot must be set here: step 2.5 path.join()s it, and
|
|
199
|
+
// path.join(undefined) throws a TypeError that nothing catches. That
|
|
200
|
+
// crash is what made `intutic daemon install` produce a crash-looping
|
|
201
|
+
// service on any machine without ~/.intutic/config.json (the launchd
|
|
202
|
+
// plist runs `connect --workspace-id --api-key` with KeepAlive=true).
|
|
203
|
+
workspaceRoot: process.cwd(),
|
|
144
204
|
};
|
|
145
205
|
}
|
|
146
206
|
if (!config) {
|
|
@@ -195,16 +255,21 @@ export async function runConnect(opts) {
|
|
|
195
255
|
// 2. AbortController for clean shutdown
|
|
196
256
|
let proxyProc = null;
|
|
197
257
|
const ac = new AbortController();
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
258
|
+
// TD-484: this handler used to abort the loop and hope. The process stayed
|
|
259
|
+
// alive for hours. `createShutdownHandler` adds the exit deadline and the
|
|
260
|
+
// second-signal exit; `terminateChild` escalates to SIGKILL.
|
|
261
|
+
const shutdown = createShutdownHandler({
|
|
262
|
+
onShutdown: () => {
|
|
263
|
+
log.info('Shutting down sync daemon...');
|
|
264
|
+
ac.abort();
|
|
265
|
+
if (proxyProc) {
|
|
266
|
+
log.info('Stopping managed proxy gateway...');
|
|
267
|
+
terminateChild(proxyProc);
|
|
268
|
+
}
|
|
269
|
+
trajectoryMonitor?.stop();
|
|
270
|
+
trajectorySubscriber?.disconnect();
|
|
271
|
+
},
|
|
272
|
+
});
|
|
208
273
|
process.on('SIGINT', shutdown);
|
|
209
274
|
process.on('SIGTERM', shutdown);
|
|
210
275
|
// 2.4. Pre-flight Valkey Validation
|
|
@@ -229,8 +294,16 @@ export async function runConnect(opts) {
|
|
|
229
294
|
}
|
|
230
295
|
// 2.5. Manage LiteLLM-Rust Proxy Gateway Process
|
|
231
296
|
const proxyPort = parseInt(process.env.PORT || '4000', 10);
|
|
297
|
+
// The daemon-side probes (`fetchEgressStatus`, `fetchGuardProbes`,
|
|
298
|
+
// `fetchLocalProxyInstanceId`) read the proxy at `INTUTIC_PROXY_URL`,
|
|
299
|
+
// defaulting to port 4000; connect knows the port it will spawn on, so an
|
|
300
|
+
// operator running on another `PORT` still gets probed at the right one.
|
|
301
|
+
process.env.INTUTIC_PROXY_URL ??= `http://127.0.0.1:${proxyPort}`;
|
|
232
302
|
let exeCmd = 'cargo';
|
|
233
303
|
let exeArgs = ['run', '--manifest-path', node_path.join(safeConfig.workspaceRoot, 'packages', 'proxy', 'Cargo.toml')];
|
|
304
|
+
// Populated only on the branch that actually spawns the proxy; the DR
|
|
305
|
+
// re-spawn below reuses it and is guarded by `proxyProc`, which is null
|
|
306
|
+
// whenever this is still empty.
|
|
234
307
|
let proxyEnv = {};
|
|
235
308
|
try {
|
|
236
309
|
const inUse = await isPortInUse(proxyPort);
|
|
@@ -278,7 +351,7 @@ export async function runConnect(opts) {
|
|
|
278
351
|
exeArgs = [];
|
|
279
352
|
}
|
|
280
353
|
catch (downloadErr) {
|
|
281
|
-
log.warn(`Auto-download failed: ${downloadErr.message}`);
|
|
354
|
+
log.warn(`Auto-download failed: ${downloadErr instanceof Error ? downloadErr.message : String(downloadErr)}`);
|
|
282
355
|
log.dim('Falling back to cargo run...');
|
|
283
356
|
}
|
|
284
357
|
}
|
|
@@ -294,6 +367,13 @@ export async function runConnect(opts) {
|
|
|
294
367
|
INTUTIC_WORKSPACE_ID: safeCreds.workspaceId,
|
|
295
368
|
INTUTIC_API_KEY: safeCreds.apiKey,
|
|
296
369
|
CONFIG_PATH: node_path.join(safeConfig.workspaceRoot, 'config.yaml'),
|
|
370
|
+
// Workspace policy: admins can disallow local Obsidian/Logseq/Foam
|
|
371
|
+
// vaults from feeding /fix. Applied at spawn; a policy change takes
|
|
372
|
+
// effect on the next connect. Vault content never leaves the machine
|
|
373
|
+
// under either setting — this only governs whether the search runs.
|
|
374
|
+
...(safeConfig.settings?.allowLocalMemoryVaults === false
|
|
375
|
+
? { INTUTIC_LOCAL_VAULTS: 'off' }
|
|
376
|
+
: {}),
|
|
297
377
|
};
|
|
298
378
|
proxyProc = spawn(exeCmd, exeArgs, {
|
|
299
379
|
cwd: safeConfig.workspaceRoot,
|
|
@@ -341,12 +421,12 @@ export async function runConnect(opts) {
|
|
|
341
421
|
log.success('Successfully trusted SSL CA certificate.');
|
|
342
422
|
}
|
|
343
423
|
catch (err) {
|
|
344
|
-
log.warn(`Failed to auto-trust SSL certificate on Windows: ${err.message}`);
|
|
424
|
+
log.warn(`Failed to auto-trust SSL certificate on Windows: ${err instanceof Error ? err.message : String(err)}`);
|
|
345
425
|
}
|
|
346
426
|
}
|
|
347
427
|
}
|
|
348
428
|
catch (err) {
|
|
349
|
-
log.warn(`Could not verify or auto-trust CA certificate: ${err.message}`);
|
|
429
|
+
log.warn(`Could not verify or auto-trust CA certificate: ${err instanceof Error ? err.message : String(err)}`);
|
|
350
430
|
}
|
|
351
431
|
}
|
|
352
432
|
}
|
|
@@ -356,10 +436,29 @@ export async function runConnect(opts) {
|
|
|
356
436
|
}
|
|
357
437
|
let localConfigVersion = safeConfig.configVersion;
|
|
358
438
|
let lastCachedConfig = null;
|
|
439
|
+
// The three gate-side caches every hook reads — the policy snapshot, the
|
|
440
|
+
// approved review-hold bypasses and the central egress policy. Refreshed on
|
|
441
|
+
// EVERY applySyncConfig (each poll and each pushed config_update), not only
|
|
442
|
+
// when the config version moved: policy changes without the config version
|
|
443
|
+
// changing, and a guardrail promoted, a hold approved or an egress mode
|
|
444
|
+
// flipped mid-session used to reach a connected machine only on restart —
|
|
445
|
+
// this function was called once, at startup (TD-488). Parity with the
|
|
446
|
+
// daemon's own loop (`syncLoop.ts`'s Steps 0b–0d). None of these throw.
|
|
447
|
+
//
|
|
448
|
+
// The snapshot also carries this workspace's `review_before:` tokens as hold
|
|
449
|
+
// rules (synced SOPs, settings and local `.intutic/sops`), so every gate —
|
|
450
|
+
// not only the Claude Code hook — holds on them (TD-474 item 4).
|
|
451
|
+
async function refreshGateCachesForConnect() {
|
|
452
|
+
const localHoldTokens = await localHoldTokensFor(safeConfig.workspaceRoot, lastCachedConfig?.sops ?? [], lastCachedConfig?.settings, safeConfig.harnesses);
|
|
453
|
+
const landed = await refreshGateCaches({ controlPlaneUrl, apiKey: safeCreds.apiKey, workspaceId: safeCreds.workspaceId, localHoldTokens });
|
|
454
|
+
if (!landed.snapshot)
|
|
455
|
+
log.dim('Policy snapshot refresh failed (will retry next sync); built-in protections are unaffected.');
|
|
456
|
+
}
|
|
359
457
|
// 3. Define configuration applier function
|
|
360
458
|
async function applySyncConfig(syncConfig, force = false) {
|
|
361
459
|
let sopsWritten = 0;
|
|
362
460
|
lastCachedConfig = syncConfig;
|
|
461
|
+
await refreshGateCachesForConnect();
|
|
363
462
|
if (syncConfig.configVersion > localConfigVersion || force) {
|
|
364
463
|
log.info(`Applying configuration v${syncConfig.configVersion}...`);
|
|
365
464
|
// Load and compile local SOP entries
|
|
@@ -399,7 +498,19 @@ export async function runConnect(opts) {
|
|
|
399
498
|
}
|
|
400
499
|
}
|
|
401
500
|
catch (err) {
|
|
402
|
-
//
|
|
501
|
+
// Only a missing `.intutic/sops` is unremarkable — most workspaces have
|
|
502
|
+
// none, and `readdir` ENOENTs on the first line of the block.
|
|
503
|
+
//
|
|
504
|
+
// Everything else was being swallowed with it, and the block goes on to
|
|
505
|
+
// read every SOP file: an unreadable file or a permissions error dropped
|
|
506
|
+
// that SOP from `localSopEntries`, and the harness configs written a few
|
|
507
|
+
// lines below were then generated *without* it, silently. Governance
|
|
508
|
+
// content going missing is the failure this whole function exists to
|
|
509
|
+
// prevent, so it does not get to be quiet.
|
|
510
|
+
const code = err instanceof Error ? err.code : undefined;
|
|
511
|
+
if (code !== 'ENOENT') {
|
|
512
|
+
log.warn(`Failed to load local SOPs from .intutic/sops — harness configs will be written without them: ${err instanceof Error ? err.message : String(err)}`);
|
|
513
|
+
}
|
|
403
514
|
}
|
|
404
515
|
const combinedSops = [...syncConfig.sops, ...localSopEntries];
|
|
405
516
|
// a. Write configs for all active harnesses
|
|
@@ -432,7 +543,10 @@ export async function runConnect(opts) {
|
|
|
432
543
|
log.warn(`Failed to inject MCP server configs: ${err instanceof Error ? err.message : String(err)}`);
|
|
433
544
|
}
|
|
434
545
|
localConfigVersion = syncConfig.configVersion;
|
|
435
|
-
|
|
546
|
+
// Mirror workspace settings locally so spawn-time policies (like
|
|
547
|
+
// allowLocalMemoryVaults) apply on the next connect without a fetch.
|
|
548
|
+
safeConfig.settings = syncConfig.settings;
|
|
549
|
+
saveConfig({ ...safeConfig, configVersion: localConfigVersion, settings: syncConfig.settings });
|
|
436
550
|
}
|
|
437
551
|
// c. Compute file hashes + update integrity store
|
|
438
552
|
const fileHashes = [];
|
|
@@ -497,14 +611,20 @@ export async function runConnect(opts) {
|
|
|
497
611
|
execSync('docker info', { stdio: 'ignore' });
|
|
498
612
|
dockerActive = true;
|
|
499
613
|
}
|
|
500
|
-
catch {
|
|
614
|
+
catch {
|
|
615
|
+
// Deliberate swallow: `docker info` exiting non-zero IS the answer we
|
|
616
|
+
// asked for — Docker is not installed or its daemon is down. That is a
|
|
617
|
+
// normal state on machines running Valkey natively, not an error to
|
|
618
|
+
// report. dockerActive stays false and we fall through to the native
|
|
619
|
+
// binary / downloaded-static healing paths below.
|
|
620
|
+
}
|
|
501
621
|
if (dockerActive) {
|
|
502
622
|
try {
|
|
503
623
|
execSync('docker start intutic-valkey', { stdio: 'ignore' });
|
|
504
624
|
log.info('[DR] Successfully sent container start command to intutic-valkey.');
|
|
505
625
|
}
|
|
506
626
|
catch (err) {
|
|
507
|
-
log.warn(`[DR] Docker container start failed: ${err.message}`);
|
|
627
|
+
log.warn(`[DR] Docker container start failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
508
628
|
}
|
|
509
629
|
}
|
|
510
630
|
else {
|
|
@@ -520,7 +640,13 @@ export async function runConnect(opts) {
|
|
|
520
640
|
nativeCmd = 'redis-server';
|
|
521
641
|
hasNativeBinary = true;
|
|
522
642
|
}
|
|
523
|
-
catch {
|
|
643
|
+
catch {
|
|
644
|
+
// Deliberate swallow: this is the last of three probes (docker,
|
|
645
|
+
// valkey-server, redis-server). `which` failing just means the
|
|
646
|
+
// binary is not on PATH, which is the question we asked.
|
|
647
|
+
// hasNativeBinary stays false and healing falls through to the
|
|
648
|
+
// downloaded static binary under ~/.intutic/bin.
|
|
649
|
+
}
|
|
524
650
|
}
|
|
525
651
|
if (hasNativeBinary) {
|
|
526
652
|
try {
|
|
@@ -529,7 +655,7 @@ export async function runConnect(opts) {
|
|
|
529
655
|
log.info(`[DR] Successfully spawned native ${nativeCmd} in background.`);
|
|
530
656
|
}
|
|
531
657
|
catch (err) {
|
|
532
|
-
log.warn(`[DR] Native daemon spawn failed: ${err.message}`);
|
|
658
|
+
log.warn(`[DR] Native daemon spawn failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
533
659
|
}
|
|
534
660
|
}
|
|
535
661
|
else {
|
|
@@ -542,7 +668,7 @@ export async function runConnect(opts) {
|
|
|
542
668
|
log.info('[DR] Successfully spawned downloaded static Valkey server in background.');
|
|
543
669
|
}
|
|
544
670
|
catch (err) {
|
|
545
|
-
log.warn(`[DR] Static binary spawn failed: ${err.message}`);
|
|
671
|
+
log.warn(`[DR] Static binary spawn failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
546
672
|
}
|
|
547
673
|
}
|
|
548
674
|
}
|
|
@@ -556,7 +682,12 @@ export async function runConnect(opts) {
|
|
|
556
682
|
const proxyActive = await isPortInUse(proxyPort);
|
|
557
683
|
if (!proxyActive) {
|
|
558
684
|
proxyStatus = 'unhealthy';
|
|
559
|
-
if (
|
|
685
|
+
if (ac.signal.aborted) {
|
|
686
|
+
// Shutting down: the proxy is gone because we stopped it. Re-spawning
|
|
687
|
+
// it here would leave an orphan holding the port (TD-484).
|
|
688
|
+
proxyStatus = 'stopped';
|
|
689
|
+
}
|
|
690
|
+
else if (proxyProc) {
|
|
560
691
|
log.warn('[DR] Managed proxy gateway process has terminated. Auto-healing re-spawn...');
|
|
561
692
|
const logDir = node_path.join(safeConfig.workspaceRoot, '.intutic', 'logs');
|
|
562
693
|
const logStream = createWriteStream(node_path.join(logDir, 'proxy-gateway.log'), { flags: 'a' });
|
|
@@ -620,6 +751,30 @@ export async function runConnect(opts) {
|
|
|
620
751
|
catch (err) {
|
|
621
752
|
log.warn(`Failed to send daemon heartbeat: ${err instanceof Error ? err.message : String(err)}`);
|
|
622
753
|
}
|
|
754
|
+
// g. Enforcement device visibility (post-strip gap #2, LLD #63
|
|
755
|
+
// hardening) — the "interval" half of "continuous compliance": this
|
|
756
|
+
// loop already runs unprivileged, as the real user, on a timer, and
|
|
757
|
+
// already just computed sslTrustStatus above for its own DR-healing
|
|
758
|
+
// purposes. Recording it as a user-scope CA-trust leg means every
|
|
759
|
+
// `intutic connect` session reports SOMETHING even on a machine that
|
|
760
|
+
// never ran `enforce apply` or `enterprise install` — best-effort,
|
|
761
|
+
// never fails the sync loop over it.
|
|
762
|
+
try {
|
|
763
|
+
await writeEnforcementState({
|
|
764
|
+
caTrust: {
|
|
765
|
+
installed: sslTrustStatus === 'trusted',
|
|
766
|
+
scope: 'user',
|
|
767
|
+
reportedAt: newIso(),
|
|
768
|
+
},
|
|
769
|
+
}, cliPkgVersion);
|
|
770
|
+
const deviceReport = await reportDeviceState({ dev: devMode });
|
|
771
|
+
if (!deviceReport.reported) {
|
|
772
|
+
log.dim(`[sync] Device report not sent: ${deviceReport.reason}`);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
catch (err) {
|
|
776
|
+
log.warn(`Failed to record/report device enforcement state: ${err instanceof Error ? err.message : String(err)}`);
|
|
777
|
+
}
|
|
623
778
|
const driftLabel = driftCount > 0 ? pc.yellow(` — ${driftCount} drift(s) detected`) : '';
|
|
624
779
|
log.dim(`[sync] Config v${localConfigVersion} — ${sopsWritten} SOPs synced${driftLabel}`);
|
|
625
780
|
// Refresh runtime env with resolved settings
|
|
@@ -631,6 +786,8 @@ export async function runConnect(opts) {
|
|
|
631
786
|
mcpProxyFailBehavior: syncConfig.settings?.mcpProxyFailBehavior,
|
|
632
787
|
mcpProxyMode: syncConfig.settings?.mcpProxyMode,
|
|
633
788
|
bypassEnforcementTier: syncConfig.settings?.bypassEnforcementTier,
|
|
789
|
+
// Shared by the MCP proxies for their session window (Wave 5.3, TD-437) — only when it is running.
|
|
790
|
+
valkeyUrl: valkeyResult.running ? valkeyUrl : undefined,
|
|
634
791
|
});
|
|
635
792
|
}
|
|
636
793
|
catch (err) {
|
|
@@ -660,8 +817,15 @@ export async function runConnect(opts) {
|
|
|
660
817
|
log.warn(`Compliance probes failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
661
818
|
}
|
|
662
819
|
};
|
|
663
|
-
//
|
|
664
|
-
|
|
820
|
+
// Drains BOTH logs, matching `services/sync-daemon/src/syncLoop.ts`.
|
|
821
|
+
//
|
|
822
|
+
// This drained hook events only. `intutic connect` runs the daemon in-process,
|
|
823
|
+
// so under the CLI runtime a `review_before` hold still blocked the tool
|
|
824
|
+
// locally and then sat in `.intutic/events/review-requests.jsonl` forever: the
|
|
825
|
+
// developer was stopped, and the decision that was supposed to become a
|
|
826
|
+
// learned rule never reached the control plane. The block working is what made
|
|
827
|
+
// it invisible.
|
|
828
|
+
const drainOnce = async () => {
|
|
665
829
|
try {
|
|
666
830
|
const drained = await drainHookEvents(safeConfig.workspaceRoot, controlPlaneUrl, safeCreds.apiKey);
|
|
667
831
|
if (drained > 0) {
|
|
@@ -671,6 +835,52 @@ export async function runConnect(opts) {
|
|
|
671
835
|
catch (err) {
|
|
672
836
|
log.warn(`[sync-daemon] Hook event drain error (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
673
837
|
}
|
|
838
|
+
// Separate try: the two go to different endpoints, and one being down says
|
|
839
|
+
// nothing about the other.
|
|
840
|
+
try {
|
|
841
|
+
const held = await drainReviewRequests(safeConfig.workspaceRoot, controlPlaneUrl, safeCreds.apiKey);
|
|
842
|
+
if (held > 0) {
|
|
843
|
+
log.info(`[sync-daemon] Drained ${held} review hold(s) to control plane`);
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
catch (err) {
|
|
847
|
+
log.warn(`[sync-daemon] Review hold drain error (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
/**
|
|
851
|
+
* Serialises drains, and coalesces the ones that arrive during one.
|
|
852
|
+
*
|
|
853
|
+
* `runDrain` is bound to `change` and `add` on **two** watched files and to a
|
|
854
|
+
* 60-second timer, and each drain reads a log and then truncates it. Those are
|
|
855
|
+
* separate awaits, so two overlapping runs both read the same lines before
|
|
856
|
+
* either truncates and the same governance events are POSTed twice — a hook
|
|
857
|
+
* incident counted once by the agent and twice by the control plane. A hook
|
|
858
|
+
* writing to both `.intutic/events/` files in the same tick is enough on its
|
|
859
|
+
* own, and that is the ordinary case: a held tool call writes the review
|
|
860
|
+
* request and the event log together.
|
|
861
|
+
*
|
|
862
|
+
* The latch matters as much as the flag. Dropping an overlapping request
|
|
863
|
+
* outright would lose the write that triggered it whenever it landed after
|
|
864
|
+
* this pass had already read the file, so the drain would wait for the next
|
|
865
|
+
* event or the 60s poll — the exact delay the watcher exists to remove.
|
|
866
|
+
*/
|
|
867
|
+
let draining = false;
|
|
868
|
+
let drainRequestedAgain = false;
|
|
869
|
+
const runDrain = async () => {
|
|
870
|
+
if (draining) {
|
|
871
|
+
drainRequestedAgain = true;
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
draining = true;
|
|
875
|
+
try {
|
|
876
|
+
do {
|
|
877
|
+
drainRequestedAgain = false;
|
|
878
|
+
await drainOnce();
|
|
879
|
+
} while (drainRequestedAgain);
|
|
880
|
+
}
|
|
881
|
+
finally {
|
|
882
|
+
draining = false;
|
|
883
|
+
}
|
|
674
884
|
};
|
|
675
885
|
// Step 0: Write runtime env file (hook scripts source this for credentials)
|
|
676
886
|
try {
|
|
@@ -678,11 +888,30 @@ export async function runConnect(opts) {
|
|
|
678
888
|
controlPlaneUrl,
|
|
679
889
|
apiKey: safeCreds.apiKey,
|
|
680
890
|
workspaceId: safeCreds.workspaceId,
|
|
891
|
+
// Shared by the MCP proxies for their session window (Wave 5.3, TD-437) — only when it is running.
|
|
892
|
+
valkeyUrl: valkeyResult.running ? valkeyUrl : undefined,
|
|
681
893
|
});
|
|
682
894
|
}
|
|
683
895
|
catch (err) {
|
|
684
896
|
log.warn(`Could not write runtime env file (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
685
897
|
}
|
|
898
|
+
// Seed the policy snapshot before the first sync cycle rather than after it.
|
|
899
|
+
//
|
|
900
|
+
// Without this, every fresh install runs its first poll interval with no
|
|
901
|
+
// snapshot — the gates enforce the static floor and nothing else. Seeding here
|
|
902
|
+
// narrows that window to "the fetch failed, and we said so" instead of "always,
|
|
903
|
+
// briefly". `refreshPolicySnapshot` never throws. Every later refresh rides
|
|
904
|
+
// `applySyncConfig` (see `refreshGateCaches`).
|
|
905
|
+
const seeded = await refreshPolicySnapshot({
|
|
906
|
+
controlPlaneUrl,
|
|
907
|
+
apiKey: safeCreds.apiKey,
|
|
908
|
+
workspaceId: safeCreds.workspaceId,
|
|
909
|
+
});
|
|
910
|
+
if (!seeded) {
|
|
911
|
+
log.warn('Could not seed the policy snapshot — governance rules from the control plane ' +
|
|
912
|
+
'will not apply until the next successful sync. Built-in protections are unaffected.');
|
|
913
|
+
}
|
|
914
|
+
await refreshGateCachesForConnect();
|
|
686
915
|
// Sync offline traces back to PostgreSQL on startup
|
|
687
916
|
try {
|
|
688
917
|
await syncOfflineTraces(controlPlaneUrl, safeCreds.apiKey);
|
|
@@ -743,25 +972,52 @@ export async function runConnect(opts) {
|
|
|
743
972
|
}).catch(() => { });
|
|
744
973
|
}
|
|
745
974
|
catch (err) {
|
|
746
|
-
//
|
|
975
|
+
// Nothing in the block above is expected to throw — the `readFile` has its
|
|
976
|
+
// own catch and `scanLocalSops` resolves to [] on failure — so this is the
|
|
977
|
+
// outer guard for the one thing that can: `node_path.join` on an undefined
|
|
978
|
+
// workspaceRoot, the TypeError that used to crash-loop the launchd service.
|
|
979
|
+
// Reported rather than swallowed, because reaching it means the startup
|
|
980
|
+
// context report never happened and the dashboard is missing this daemon.
|
|
981
|
+
log.warn(`Failed to send initial context report: ${err instanceof Error ? err.message : String(err)}`);
|
|
747
982
|
}
|
|
748
|
-
// FSEvents-driven
|
|
983
|
+
// FSEvents-driven drain, watching BOTH logs.
|
|
984
|
+
//
|
|
985
|
+
// Watching only hook events meant a review hold waited for the 60s safety
|
|
986
|
+
// timer at best — and, before `runDrain` learned to drain holds at all, never.
|
|
749
987
|
const hookEventsLog = node_path.join(safeConfig.workspaceRoot, '.intutic', 'events', 'hook-events.jsonl');
|
|
988
|
+
const reviewHoldsLog = node_path.join(safeConfig.workspaceRoot, REVIEW_REQUESTS_LOG);
|
|
750
989
|
let fsWatcher = null;
|
|
751
|
-
let drainSafetyTimer = null;
|
|
752
990
|
try {
|
|
753
991
|
await node_fs.mkdir(node_path.dirname(hookEventsLog), { recursive: true });
|
|
754
|
-
fsWatcher = watch(hookEventsLog, { ignoreInitial: true, persistent: false });
|
|
992
|
+
fsWatcher = watch([hookEventsLog, reviewHoldsLog], { ignoreInitial: true, persistent: false });
|
|
755
993
|
fsWatcher.on('change', runDrain);
|
|
756
994
|
fsWatcher.on('add', runDrain);
|
|
757
995
|
}
|
|
758
996
|
catch (err) {
|
|
759
|
-
//
|
|
997
|
+
// A real fallback, not a swallow: `drainSafetyTimer` below drains every 60 s
|
|
998
|
+
// regardless. But losing the FSEvents watcher turns a near-immediate drain
|
|
999
|
+
// into a up-to-60-second one, which is the difference between a hook
|
|
1000
|
+
// incident showing up while the agent is still running and after it exited.
|
|
1001
|
+
// Say so, at dim, so the degradation is diagnosable.
|
|
1002
|
+
log.dim(`Hook-event watcher unavailable, falling back to the 60s drain poll: ${err instanceof Error ? err.message : String(err)}`);
|
|
760
1003
|
}
|
|
761
1004
|
// 60-second safety-net drain poll
|
|
762
|
-
drainSafetyTimer = setInterval(runDrain, 60_000);
|
|
1005
|
+
const drainSafetyTimer = setInterval(runDrain, 60_000);
|
|
763
1006
|
// Run initial compliance check on startup
|
|
764
1007
|
await runProbes();
|
|
1008
|
+
// dsh coverage-gap visibility (TD-370): once at startup, not every poll
|
|
1009
|
+
// tick — the gap only changes state on the user's first `dsh --profile
|
|
1010
|
+
// <name>` run, which the filesystem watcher below reacts to immediately
|
|
1011
|
+
// via settingsGuard.ts's isDshProfilesRoot `addDir` handling. Only worth
|
|
1012
|
+
// checking when dsh is actually a configured harness for this workspace.
|
|
1013
|
+
if (safeConfig.harnesses.includes('dsh')) {
|
|
1014
|
+
try {
|
|
1015
|
+
await warnIfDshCoverageGap();
|
|
1016
|
+
}
|
|
1017
|
+
catch (err) {
|
|
1018
|
+
log.dim(`dsh coverage-gap check failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
765
1021
|
// 5. Start Filesystem Watcher
|
|
766
1022
|
const watcher = startWatcher(safeConfig.workspaceRoot, safeConfig.harnesses, async (changedPath) => {
|
|
767
1023
|
const filename = node_path.basename(changedPath);
|
|
@@ -793,8 +1049,19 @@ export async function runConnect(opts) {
|
|
|
793
1049
|
}
|
|
794
1050
|
return;
|
|
795
1051
|
}
|
|
796
|
-
// B.
|
|
797
|
-
|
|
1052
|
+
// B. Tamper detection for every governed config, not just Claude Code's.
|
|
1053
|
+
//
|
|
1054
|
+
// This used to filter on `filename === 'settings.json'`, which silently disabled
|
|
1055
|
+
// five of the six branches inside guardSettingsFile: the Cursor, Windsurf, Cline
|
|
1056
|
+
// and OpenHands restores all key off `hooks.json`, and the Goose
|
|
1057
|
+
// governance-plugin override incident keys off a path containing
|
|
1058
|
+
// `intutic-governance`. None of those basenames could ever reach the guard, so a
|
|
1059
|
+
// tampered hooks file on any harness but Claude Code was a no-op — and a tampered
|
|
1060
|
+
// Windsurf/VS Code/Gemini settings file passed the basename test only to fall
|
|
1061
|
+
// through and log `settings_intact`.
|
|
1062
|
+
//
|
|
1063
|
+
// Route anything on the governed list to the guard and let it decide.
|
|
1064
|
+
if (isGovernedConfigPath(changedPath, filename)) {
|
|
798
1065
|
try {
|
|
799
1066
|
const sops = lastCachedConfig?.sops ?? [];
|
|
800
1067
|
const tampered = await guardSettingsFile(changedPath, safeConfig.workspaceRoot, sops);
|
|
@@ -868,7 +1135,50 @@ export async function runConnect(opts) {
|
|
|
868
1135
|
try {
|
|
869
1136
|
await syncOfflineTraces(controlPlaneUrl, safeCreds.apiKey);
|
|
870
1137
|
}
|
|
871
|
-
catch {
|
|
1138
|
+
catch (err) {
|
|
1139
|
+
// Non-fatal by design: a backlog of offline traces must never stop the
|
|
1140
|
+
// sync iteration — the next tick retries the same backlog. But it is not
|
|
1141
|
+
// a no-op either, so report it the way the identical startup call above
|
|
1142
|
+
// does, at dim level because this runs every poll interval.
|
|
1143
|
+
log.dim(`Offline trace sync failed (will retry next poll): ${err instanceof Error ? err.message : String(err)}`);
|
|
1144
|
+
}
|
|
1145
|
+
// Register agents + report one session per harness (the reporter
|
|
1146
|
+
// dedupes per run and per proxy process). connect's inline loop bypassed
|
|
1147
|
+
// both, leaving the dashboard graph empty for the primary user path.
|
|
1148
|
+
// The local proxy's instance id is read once per iteration: with it the
|
|
1149
|
+
// harness's git/task context lands on the proxy's own session row, the
|
|
1150
|
+
// one its traces are filed under (TD-231, Wave 5.6).
|
|
1151
|
+
const proxyInstanceId = safeConfig.harnesses.length > 0 ? await fetchLocalProxyInstanceId() : null;
|
|
1152
|
+
for (const harnessType of safeConfig.harnesses) {
|
|
1153
|
+
try {
|
|
1154
|
+
const report = await collectAgentReport({
|
|
1155
|
+
workspaceRoot: process.cwd(),
|
|
1156
|
+
harnessType,
|
|
1157
|
+
configSynced: true,
|
|
1158
|
+
dlpEnabled: true,
|
|
1159
|
+
policyEnforced: true,
|
|
1160
|
+
allowLocalVaults: syncConfig.settings?.allowLocalMemoryVaults,
|
|
1161
|
+
});
|
|
1162
|
+
await reportAgent(controlPlaneUrl, safeCreds.apiKey, safeCreds.workspaceId, report);
|
|
1163
|
+
await startHarnessSession({
|
|
1164
|
+
controlPlaneUrl,
|
|
1165
|
+
apiKey: safeCreds.apiKey,
|
|
1166
|
+
workspaceId: safeCreds.workspaceId,
|
|
1167
|
+
harnessType,
|
|
1168
|
+
workspaceRoot: process.cwd(),
|
|
1169
|
+
...(proxyInstanceId ? { proxyInstanceId } : {}),
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
catch (err) {
|
|
1173
|
+
// Per-harness isolation is deliberate: one harness failing to register
|
|
1174
|
+
// must not stop the others or abort the poll iteration. Silence was
|
|
1175
|
+
// not deliberate — fetchConfig already succeeded above, so a failure
|
|
1176
|
+
// here means the control plane accepted the config read and rejected
|
|
1177
|
+
// the agent report, which is exactly the "dashboard graph is empty"
|
|
1178
|
+
// symptom this loop was added to fix. Report it so it is diagnosable.
|
|
1179
|
+
log.dim(`Agent report/session for harness '${harnessType}' failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
872
1182
|
// Run compliance probes on each iteration
|
|
873
1183
|
await runProbes();
|
|
874
1184
|
}
|
|
@@ -888,9 +1198,9 @@ export async function runConnect(opts) {
|
|
|
888
1198
|
// Cleanup watcher, intervals, and WS connection on exit
|
|
889
1199
|
watcher.stop();
|
|
890
1200
|
fsWatcher?.close();
|
|
891
|
-
|
|
892
|
-
clearInterval(drainSafetyTimer);
|
|
1201
|
+
clearInterval(drainSafetyTimer);
|
|
893
1202
|
wsClient.close();
|
|
1203
|
+
await endAllOpenSessions(controlPlaneUrl, safeCreds.apiKey);
|
|
894
1204
|
log.success('Sync daemon stopped.');
|
|
895
1205
|
}
|
|
896
1206
|
async function scanLocalSops(workspaceRoot) {
|